diff --git a/app/build.gradle b/app/build.gradle
index effc1d64e..ac6879953 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -202,7 +202,7 @@ dependencies {
implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation 'androidx.core:core-ktx:1.7.0'
- def nav_version = "2.4.0"
+ def nav_version = "2.4.1"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
@@ -211,7 +211,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
- implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "androidx.security:security-crypto-ktx:1.1.0-alpha03"
implementation 'androidx.core:core-splashscreen:1.0.0-beta01'
@@ -237,7 +237,7 @@ dependencies {
implementation 'com.google.firebase:firebase-messaging'
}
- implementation 'org.linphone:linphone-sdk-android:5.1+'
+ implementation 'org.linphone:linphone-sdk-android:5.2+'
// Only enable leak canary prior to release
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 88547b8ef..571c013c6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -115,19 +115,11 @@
-
-
-
-
-
+
0
0
+ 0
diff --git a/app/src/main/assets/assistant_linphone_default_values b/app/src/main/assets/assistant_linphone_default_values
index 9f2badee0..497e98efc 100644
--- a/app/src/main/assets/assistant_linphone_default_values
+++ b/app/src/main/assets/assistant_linphone_default_values
@@ -15,8 +15,10 @@
nat_policy_default_values
sip.linphone.org
sip:conference-factory@sip.linphone.org
+ sip:videoconference-factory2@sip.linphone.org
1
1
+ 1
stun.linphone.org
diff --git a/app/src/main/assets/linphonerc_default b/app/src/main/assets/linphonerc_default
index 48add4247..2e8b8b02e 100644
--- a/app/src/main/assets/linphonerc_default
+++ b/app/src/main/assets/linphonerc_default
@@ -33,6 +33,7 @@ file_transfer_server_url=https://www.linphone.org:444/lft.php
version_check_url_root=https://www.linphone.org/releases
max_calls=10
history_max_size=100
+conference_layout=1
[in-app-purchase]
server_url=https://subscribe.linphone.org:444/inapp.php
diff --git a/app/src/main/java/org/linphone/activities/Navigation.kt b/app/src/main/java/org/linphone/activities/Navigation.kt
index 5d6bd190a..331c2417f 100644
--- a/app/src/main/java/org/linphone/activities/Navigation.kt
+++ b/app/src/main/java/org/linphone/activities/Navigation.kt
@@ -36,15 +36,22 @@ import org.linphone.activities.main.chat.fragments.ChatRoomCreationFragment
import org.linphone.activities.main.chat.fragments.DetailChatRoomFragment
import org.linphone.activities.main.chat.fragments.GroupInfoFragment
import org.linphone.activities.main.chat.fragments.MasterChatRoomsFragment
+import org.linphone.activities.main.conference.fragments.*
import org.linphone.activities.main.contact.fragments.ContactEditorFragment
import org.linphone.activities.main.contact.fragments.DetailContactFragment
import org.linphone.activities.main.contact.fragments.MasterContactsFragment
import org.linphone.activities.main.dialer.fragments.DialerFragment
import org.linphone.activities.main.fragments.TabsFragment
import org.linphone.activities.main.history.fragments.DetailCallLogFragment
+import org.linphone.activities.main.history.fragments.DetailConferenceCallLogFragment
import org.linphone.activities.main.history.fragments.MasterCallLogsFragment
import org.linphone.activities.main.settings.fragments.*
import org.linphone.activities.main.sidemenu.fragments.SideMenuFragment
+import org.linphone.activities.voip.CallActivity
+import org.linphone.activities.voip.fragments.ActiveCallOrConferenceFragment
+import org.linphone.activities.voip.fragments.ConferenceParticipantsFragment
+import org.linphone.activities.voip.fragments.IncomingCallFragment
+import org.linphone.activities.voip.fragments.OutgoingCallFragment
import org.linphone.contact.NativeContact
import org.linphone.core.Address
@@ -173,6 +180,104 @@ internal fun DialerFragment.navigateToConfigFileViewer() {
)
}
+internal fun DialerFragment.navigateToConferenceScheduling() {
+ findMasterNavController().navigate(
+ R.id.action_global_conferenceSchedulingFragment,
+ null,
+ popupTo()
+ )
+}
+
+/* Conference scheduling related */
+
+internal fun ConferenceSchedulingFragment.navigateToParticipantsList() {
+ if (findNavController().currentDestination?.id == R.id.conferenceSchedulingFragment) {
+ findNavController().navigate(
+ R.id.action_conferenceSchedulingFragment_to_conferenceSchedulingParticipantsListFragment,
+ null,
+ popupTo(R.id.conferenceSchedulingParticipantsListFragment, true)
+ )
+ }
+}
+
+internal fun ConferenceSchedulingParticipantsListFragment.navigateToSummary() {
+ if (findNavController().currentDestination?.id == R.id.conferenceSchedulingParticipantsListFragment) {
+ findNavController().navigate(
+ R.id.action_conferenceSchedulingParticipantsListFragment_to_conferenceSchedulingSummaryFragment,
+ null,
+ popupTo(R.id.conferenceSchedulingSummaryFragment, true)
+ )
+ }
+}
+
+internal fun ConferenceSchedulingSummaryFragment.goToScheduledConferences() {
+ if (findNavController().currentDestination?.id == R.id.conferenceSchedulingSummaryFragment) {
+ findNavController().navigate(
+ R.id.action_global_scheduledConferencesFragment,
+ null,
+ popupTo(R.id.dialerFragment, false)
+ )
+ }
+}
+
+internal fun ConferenceSchedulingSummaryFragment.navigateToConferenceWaitingRoom(
+ address: String,
+ subject: String?
+) {
+ val bundle = Bundle()
+ bundle.putString("Address", address)
+ bundle.putString("Subject", subject)
+ findMasterNavController().navigate(
+ R.id.action_global_conferenceWaitingRoomFragment,
+ bundle,
+ popupTo(R.id.dialerFragment, false)
+ )
+}
+
+internal fun ConferenceWaitingRoomFragment.navigateToDialer() {
+ findNavController().navigate(
+ R.id.action_global_dialerFragment,
+ null,
+ popupTo(R.id.dialerFragment, true)
+ )
+}
+
+internal fun DetailChatRoomFragment.navigateToConferenceWaitingRoom(
+ address: String,
+ subject: String?
+) {
+ val bundle = Bundle()
+ bundle.putString("Address", address)
+ bundle.putString("Subject", subject)
+ findMasterNavController().navigate(
+ R.id.action_global_conferenceWaitingRoomFragment,
+ bundle,
+ popupTo(R.id.conferenceWaitingRoomFragment, true)
+ )
+}
+
+internal fun ScheduledConferencesFragment.navigateToConferenceWaitingRoom(
+ address: String,
+ subject: String?
+) {
+ val bundle = Bundle()
+ bundle.putString("Address", address)
+ bundle.putString("Subject", subject)
+ findMasterNavController().navigate(
+ R.id.action_global_conferenceWaitingRoomFragment,
+ bundle,
+ popupTo(R.id.conferenceWaitingRoomFragment, true)
+ )
+}
+
+internal fun ScheduledConferencesFragment.navigateToConferenceScheduling() {
+ findMasterNavController().navigate(
+ R.id.action_global_conferenceSchedulingFragment,
+ null,
+ popupTo(R.id.conferenceSchedulingFragment, true)
+ )
+}
+
/* Chat related */
internal fun MasterChatRoomsFragment.navigateToChatRoom(args: Bundle) {
@@ -499,6 +604,19 @@ internal fun MasterCallLogsFragment.navigateToCallHistory(slidingPane: SlidingPa
}
}
+internal fun MasterCallLogsFragment.navigateToConferenceCallHistory(slidingPane: SlidingPaneLayout) {
+ if (findNavController().currentDestination?.id == R.id.masterCallLogsFragment) {
+ val navHostFragment =
+ childFragmentManager.findFragmentById(R.id.history_nav_container) as NavHostFragment
+ navHostFragment.navController.navigate(
+ R.id.action_global_detailConferenceCallLogFragment,
+ null,
+ popupTo(R.id.detailConferenceCallLogFragment, true)
+ )
+ if (!slidingPane.isOpen) slidingPane.openPane()
+ }
+}
+
internal fun MasterCallLogsFragment.clearDisplayedCallHistory() {
if (findNavController().currentDestination?.id == R.id.masterCallLogsFragment) {
val navHostFragment =
@@ -519,6 +637,20 @@ internal fun MasterCallLogsFragment.navigateToDialer(args: Bundle?) {
)
}
+internal fun MasterCallLogsFragment.navigateToConferenceWaitingRoom(
+ address: String,
+ subject: String?
+) {
+ val bundle = Bundle()
+ bundle.putString("Address", address)
+ bundle.putString("Subject", subject)
+ findMasterNavController().navigate(
+ R.id.action_global_conferenceWaitingRoomFragment,
+ bundle,
+ popupTo(R.id.dialerFragment, false)
+ )
+}
+
internal fun DetailCallLogFragment.navigateToContacts(sipUriToAdd: String) {
val deepLink = "linphone-android://contact/new/$sipUriToAdd"
findMasterNavController().navigate(Uri.parse(deepLink))
@@ -564,6 +696,16 @@ internal fun DetailCallLogFragment.navigateToEmptyCallHistory() {
}
}
+internal fun DetailConferenceCallLogFragment.navigateToEmptyCallHistory() {
+ if (findNavController().currentDestination?.id == R.id.detailConferenceCallLogFragment) {
+ findNavController().navigate(
+ R.id.action_global_emptyFragment,
+ null,
+ popupTo(R.id.emptyCallHistoryFragment, true)
+ )
+ }
+}
+
/* Settings related */
internal fun SettingsFragment.navigateToAccountSettings(identity: String) {
@@ -683,6 +825,19 @@ internal fun SettingsFragment.navigateToAdvancedSettings(slidingPane: SlidingPan
}
}
+internal fun SettingsFragment.navigateToConferencesSettings(slidingPane: SlidingPaneLayout) {
+ if (findNavController().currentDestination?.id == R.id.settingsFragment) {
+ val navHostFragment =
+ childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment
+ navHostFragment.navController.navigate(
+ R.id.action_global_conferencesSettingsFragment,
+ null,
+ popupTo(R.id.conferencesSettingsFragment, true)
+ )
+ if (!slidingPane.isOpen) slidingPane.openPane()
+ }
+}
+
internal fun AccountSettingsFragment.navigateToPhoneLinking(args: Bundle?) {
if (findNavController().currentDestination?.id == R.id.accountSettingsFragment) {
findNavController().navigate(
@@ -731,6 +886,10 @@ internal fun ChatSettingsFragment.navigateToEmptySetting() {
navigateToEmptySetting(findNavController())
}
+internal fun ConferencesSettingsFragment.navigateToEmptySetting() {
+ navigateToEmptySetting(findNavController())
+}
+
internal fun ContactsSettingsFragment.navigateToEmptySetting() {
navigateToEmptySetting(findNavController())
}
@@ -778,6 +937,110 @@ internal fun SideMenuFragment.navigateToRecordings() {
)
}
+internal fun SideMenuFragment.navigateToScheduledConferences() {
+ findNavController().navigate(
+ R.id.action_global_scheduledConferencesFragment,
+ null,
+ popupTo(R.id.scheduledConferencesFragment, true)
+ )
+}
+
+/* Calls related */
+
+internal fun CallActivity.navigateToActiveCall() {
+ if (findNavController(R.id.nav_host_fragment).currentDestination?.id != R.id.activeCallOrConferenceFragment) {
+ findNavController(R.id.nav_host_fragment).navigate(
+ R.id.action_global_activeCallOrConferenceFragment,
+ null,
+ popupTo(R.id.activeCallOrConferenceFragment, false)
+ )
+ }
+}
+
+internal fun CallActivity.navigateToOutgoingCall() {
+ findNavController(R.id.nav_host_fragment).navigate(
+ R.id.action_global_outgoingCallFragment,
+ null,
+ popupTo(R.id.activeCallOrConferenceFragment, false)
+ )
+}
+
+internal fun CallActivity.navigateToIncomingCall(earlyMediaVideoEnabled: Boolean) {
+ val args = Bundle()
+ args.putBoolean("earlyMediaVideo", earlyMediaVideoEnabled)
+ findNavController(R.id.nav_host_fragment).navigate(
+ R.id.action_global_incomingCallFragment,
+ args,
+ popupTo(R.id.activeCallOrConferenceFragment, false)
+ )
+}
+
+internal fun OutgoingCallFragment.navigateToActiveCall() {
+ findNavController().navigate(
+ R.id.action_global_activeCallOrConferenceFragment,
+ null,
+ popupTo(R.id.activeCallOrConferenceFragment, false)
+ )
+}
+
+internal fun IncomingCallFragment.navigateToActiveCall() {
+ findNavController().navigate(
+ R.id.action_global_activeCallOrConferenceFragment,
+ null,
+ popupTo(R.id.activeCallOrConferenceFragment, false)
+ )
+}
+
+internal fun ActiveCallOrConferenceFragment.navigateToCallsList() {
+ if (findNavController().currentDestination?.id == R.id.activeCallOrConferenceFragment) {
+ findNavController().navigate(
+ R.id.action_activeCallOrConferenceFragment_to_callsListFragment,
+ null,
+ popupTo()
+ )
+ }
+}
+
+internal fun ActiveCallOrConferenceFragment.navigateToConferenceParticipants() {
+ if (findNavController().currentDestination?.id == R.id.activeCallOrConferenceFragment) {
+ findNavController().navigate(
+ R.id.action_activeCallOrConferenceFragment_to_conferenceParticipantsFragment,
+ null,
+ popupTo()
+ )
+ }
+}
+
+internal fun ActiveCallOrConferenceFragment.navigateToChat(args: Bundle) {
+ if (findNavController().currentDestination?.id == R.id.activeCallOrConferenceFragment) {
+ findNavController().navigate(
+ R.id.action_activeCallOrConferenceFragment_to_chatFragment,
+ args,
+ popupTo()
+ )
+ }
+}
+
+internal fun ActiveCallOrConferenceFragment.navigateToConferenceLayout() {
+ if (findNavController().currentDestination?.id == R.id.activeCallOrConferenceFragment) {
+ findNavController().navigate(
+ R.id.action_activeCallOrConferenceFragment_to_conferenceLayoutFragment,
+ null,
+ popupTo()
+ )
+ }
+}
+
+internal fun ConferenceParticipantsFragment.navigateToAddParticipants() {
+ if (findNavController().currentDestination?.id == R.id.conferenceParticipantsFragment) {
+ findNavController().navigate(
+ R.id.action_conferenceParticipantsFragment_to_conferenceAddParticipantsFragment,
+ null,
+ popupTo(R.id.conferenceAddParticipantsFragment, true)
+ )
+ }
+}
+
/* Assistant related */
internal fun WelcomeFragment.navigateToEmailAccountCreation() {
diff --git a/app/src/main/java/org/linphone/activities/call/ProximitySensorActivity.kt b/app/src/main/java/org/linphone/activities/ProximitySensorActivity.kt
similarity index 95%
rename from app/src/main/java/org/linphone/activities/call/ProximitySensorActivity.kt
rename to app/src/main/java/org/linphone/activities/ProximitySensorActivity.kt
index cc502b4d1..e66befe35 100644
--- a/app/src/main/java/org/linphone/activities/call/ProximitySensorActivity.kt
+++ b/app/src/main/java/org/linphone/activities/ProximitySensorActivity.kt
@@ -17,14 +17,13 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.linphone.activities.call
+package org.linphone.activities
import android.content.Context
import android.os.Bundle
import android.os.PowerManager
import android.os.PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY
import org.linphone.LinphoneApplication.Companion.coreContext
-import org.linphone.activities.GenericActivity
import org.linphone.core.tools.Log
abstract class ProximitySensorActivity : GenericActivity() {
@@ -49,7 +48,7 @@ abstract class ProximitySensorActivity : GenericActivity() {
super.onResume()
if (coreContext.core.callsNb > 0) {
- val videoEnabled = coreContext.isVideoCallOrConferenceActive()
+ val videoEnabled = coreContext.core.currentCall?.currentParams?.isVideoEnabled ?: false
enableProximitySensor(!videoEnabled)
}
}
diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt
index 1f7fa1f26..66d1ed98a 100644
--- a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt
@@ -22,7 +22,7 @@ package org.linphone.activities.assistant.fragments
import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModelProvider
-import org.linphone.LinphoneApplication
+import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.activities.assistant.AssistantActivity
import org.linphone.activities.assistant.viewmodels.*
@@ -87,7 +87,7 @@ class PhoneAccountLinkingFragment : AbstractPhoneFragment.
- */
-package org.linphone.activities.call
-
-import android.content.Intent
-import android.content.res.Configuration
-import android.content.res.Resources
-import android.os.Bundle
-import android.view.Gravity
-import android.view.View
-import androidx.constraintlayout.widget.ConstraintSet
-import androidx.databinding.DataBindingUtil
-import androidx.lifecycle.ViewModelProvider
-import androidx.window.layout.FoldingFeature
-import kotlinx.coroutines.*
-import org.linphone.LinphoneApplication.Companion.coreContext
-import org.linphone.LinphoneApplication.Companion.corePreferences
-import org.linphone.R
-import org.linphone.activities.call.viewmodels.*
-import org.linphone.activities.main.MainActivity
-import org.linphone.compatibility.Compatibility
-import org.linphone.core.tools.Log
-import org.linphone.databinding.CallActivityBinding
-
-class CallActivity : ProximitySensorActivity() {
- private lateinit var binding: CallActivityBinding
- private lateinit var viewModel: ControlsFadingViewModel
- private lateinit var sharedViewModel: SharedCallViewModel
-
- private var foldingFeature: FoldingFeature? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- Compatibility.setShowWhenLocked(this, true)
- Compatibility.setTurnScreenOn(this, true)
-
- binding = DataBindingUtil.setContentView(this, R.layout.call_activity)
- binding.lifecycleOwner = this
-
- viewModel = ViewModelProvider(this)[ControlsFadingViewModel::class.java]
- binding.controlsFadingViewModel = viewModel
-
- sharedViewModel = ViewModelProvider(this)[SharedCallViewModel::class.java]
-
- sharedViewModel.toggleDrawerEvent.observe(
- this
- ) {
- it.consume {
- if (binding.statsMenu.isDrawerOpen(Gravity.LEFT)) {
- binding.statsMenu.closeDrawer(binding.sideMenuContent, true)
- } else {
- binding.statsMenu.openDrawer(binding.sideMenuContent, true)
- }
- }
- }
-
- sharedViewModel.resetHiddenInterfaceTimerInVideoCallEvent.observe(
- this
- ) {
- it.consume {
- viewModel.showMomentarily()
- }
- }
-
- viewModel.proximitySensorEnabled.observe(
- this
- ) {
- enableProximitySensor(it)
- }
-
- viewModel.videoEnabled.observe(
- this
- ) {
- updateConstraintSetDependingOnFoldingState()
- }
- }
-
- override fun onLayoutChanges(foldingFeature: FoldingFeature?) {
- this.foldingFeature = foldingFeature
- updateConstraintSetDependingOnFoldingState()
- }
-
- override fun onResume() {
- super.onResume()
-
- if (coreContext.core.callsNb == 0) {
- Log.w("[Call Activity] Resuming but no call found...")
- if (isTaskRoot) {
- // When resuming app from recent tasks make sure MainActivity will be launched if there is no call
- val intent = Intent()
- intent.setClass(this, MainActivity::class.java)
- intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
- startActivity(intent)
- } else {
- finish()
- }
- } else {
- coreContext.removeCallOverlay()
- }
-
- if (corePreferences.fullScreenCallUI) {
- hideSystemUI()
- window.decorView.setOnSystemUiVisibilityChangeListener { visibility ->
- if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) {
- GlobalScope.launch {
- delay(2000)
- withContext(Dispatchers.Main) {
- hideSystemUI()
- }
- }
- }
- }
- }
- }
-
- override fun onPause() {
- val core = coreContext.core
- if (core.callsNb > 0) {
- coreContext.createCallOverlay()
- }
-
- super.onPause()
- }
-
- override fun onDestroy() {
- coreContext.core.nativeVideoWindowId = null
- coreContext.core.nativePreviewWindowId = null
-
- super.onDestroy()
- }
-
- override fun onUserLeaveHint() {
- super.onUserLeaveHint()
-
- if (coreContext.isVideoCallOrConferenceActive()) {
- Compatibility.enterPipMode(this)
- }
- }
-
- override fun onPictureInPictureModeChanged(
- isInPictureInPictureMode: Boolean,
- newConfig: Configuration
- ) {
- if (isInPictureInPictureMode) {
- viewModel.areControlsHidden.value = true
- }
-
- if (corePreferences.hideCameraPreviewInPipMode) {
- viewModel.isVideoPreviewHidden.value = isInPictureInPictureMode
- } else {
- viewModel.isVideoPreviewResizedForPip.value = isInPictureInPictureMode
- }
- }
-
- override fun getTheme(): Resources.Theme {
- val theme = super.getTheme()
- if (corePreferences.fullScreenCallUI) {
- theme.applyStyle(R.style.FullScreenTheme, true)
- }
- return theme
- }
-
- private fun hideSystemUI() {
- window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or
- View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
- View.SYSTEM_UI_FLAG_IMMERSIVE or
- View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
- View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
- View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
- }
-
- private fun updateConstraintSetDependingOnFoldingState() {
- val feature = foldingFeature ?: return
- val constraintLayout = binding.constraintLayout
- val set = ConstraintSet()
- set.clone(constraintLayout)
-
- if (feature.state == FoldingFeature.State.HALF_OPENED && viewModel.videoEnabled.value == true) {
- set.setGuidelinePercent(R.id.hinge_top, 0.5f)
- set.setGuidelinePercent(R.id.hinge_bottom, 0.5f)
- viewModel.disable(true)
- } else {
- set.setGuidelinePercent(R.id.hinge_top, 0f)
- set.setGuidelinePercent(R.id.hinge_bottom, 1f)
- viewModel.disable(false)
- }
-
- set.applyTo(constraintLayout)
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/call/IncomingCallActivity.kt b/app/src/main/java/org/linphone/activities/call/IncomingCallActivity.kt
deleted file mode 100644
index c150de7a0..000000000
--- a/app/src/main/java/org/linphone/activities/call/IncomingCallActivity.kt
+++ /dev/null
@@ -1,183 +0,0 @@
-/*
- * Copyright (c) 2010-2020 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.linphone.activities.call
-
-import android.Manifest
-import android.annotation.TargetApi
-import android.app.KeyguardManager
-import android.content.Context
-import android.content.Intent
-import android.content.pm.ActivityInfo
-import android.content.pm.PackageManager
-import android.os.Bundle
-import androidx.databinding.DataBindingUtil
-import androidx.lifecycle.ViewModelProvider
-import org.linphone.LinphoneApplication.Companion.coreContext
-import org.linphone.R
-import org.linphone.activities.GenericActivity
-import org.linphone.activities.call.viewmodels.IncomingCallViewModel
-import org.linphone.activities.call.viewmodels.IncomingCallViewModelFactory
-import org.linphone.activities.main.MainActivity
-import org.linphone.compatibility.Compatibility
-import org.linphone.core.Call
-import org.linphone.core.tools.Log
-import org.linphone.databinding.CallIncomingActivityBinding
-import org.linphone.mediastream.Version
-import org.linphone.utils.PermissionHelper
-
-class IncomingCallActivity : GenericActivity() {
- private lateinit var binding: CallIncomingActivityBinding
- private lateinit var viewModel: IncomingCallViewModel
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- Compatibility.setShowWhenLocked(this, true)
- Compatibility.setTurnScreenOn(this, true)
- // Leaks on API 27+: https://stackoverflow.com/questions/60477120/keyguardmanager-memory-leak
- Compatibility.requestDismissKeyguard(this)
-
- binding = DataBindingUtil.setContentView(this, R.layout.call_incoming_activity)
- binding.lifecycleOwner = this
-
- val incomingCall: Call? = findIncomingCall()
- if (incomingCall == null) {
- Log.e("[Incoming Call Activity] Couldn't find call in state Incoming")
- if (isTaskRoot) {
- // When resuming app from recent tasks make sure MainActivity will be launched if there is no call
- val intent = Intent()
- intent.setClass(this, MainActivity::class.java)
- intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
- startActivity(intent)
- } else {
- finish()
- }
- return
- }
-
- viewModel = ViewModelProvider(
- this,
- IncomingCallViewModelFactory(incomingCall)
- )[IncomingCallViewModel::class.java]
- binding.viewModel = viewModel
-
- viewModel.callEndedEvent.observe(
- this
- ) {
- it.consume {
- Log.i("[Incoming Call Activity] Call ended, finish activity")
- finish()
- }
- }
-
- viewModel.earlyMediaVideoEnabled.observe(
- this
- ) {
- if (it) {
- Log.i("[Incoming Call Activity] Early media video being received, set native window id")
- coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface
- }
- }
-
- val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
- val keyguardLocked = keyguardManager.isKeyguardLocked
- viewModel.screenLocked.value = keyguardLocked
- if (keyguardLocked) {
- // Forbid screen rotation to prevent keyguard to show up above incoming call view
- requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
- }
-
- binding.buttons.setViewModel(viewModel)
-
- if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
- checkPermissions()
- }
- }
-
- override fun onResume() {
- super.onResume()
-
- val incomingCall: Call? = findIncomingCall()
- if (incomingCall == null) {
- Log.e("[Incoming Call Activity] Couldn't find call in state Incoming")
- if (isTaskRoot) {
- // When resuming app from recent tasks make sure MainActivity will be launched if there is no call
- val intent = Intent()
- intent.setClass(this, MainActivity::class.java)
- intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
- startActivity(intent)
- } else {
- finish()
- }
- }
- }
-
- @TargetApi(Version.API23_MARSHMALLOW_60)
- private fun checkPermissions() {
- val permissionsRequiredList = arrayListOf()
- if (!PermissionHelper.get().hasRecordAudioPermission()) {
- Log.i("[Incoming Call Activity] Asking for RECORD_AUDIO permission")
- permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO)
- }
-
- if (viewModel.call.currentParams.isVideoEnabled && !PermissionHelper.get().hasCameraPermission()) {
- Log.i("[Incoming Call Activity] Asking for CAMERA permission")
- permissionsRequiredList.add(Manifest.permission.CAMERA)
- }
-
- if (permissionsRequiredList.isNotEmpty()) {
- val permissionsRequired = arrayOfNulls(permissionsRequiredList.size)
- permissionsRequiredList.toArray(permissionsRequired)
- requestPermissions(permissionsRequired, 0)
- }
- }
-
- override fun onRequestPermissionsResult(
- requestCode: Int,
- permissions: Array,
- grantResults: IntArray
- ) {
- if (requestCode == 0) {
- for (i in permissions.indices) {
- when (permissions[i]) {
- Manifest.permission.RECORD_AUDIO -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
- Log.i("[Incoming Call Activity] RECORD_AUDIO permission has been granted")
- }
- Manifest.permission.CAMERA -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
- Log.i("[Incoming Call Activity] CAMERA permission has been granted")
- coreContext.core.reloadVideoDevices()
- }
- }
- }
- }
- super.onRequestPermissionsResult(requestCode, permissions, grantResults)
- }
-
- private fun findIncomingCall(): Call? {
- for (call in coreContext.core.calls) {
- if (call.state == Call.State.IncomingReceived ||
- call.state == Call.State.IncomingEarlyMedia
- ) {
- return call
- }
- }
- return null
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/call/OutgoingCallActivity.kt b/app/src/main/java/org/linphone/activities/call/OutgoingCallActivity.kt
deleted file mode 100644
index 5dc2a1676..000000000
--- a/app/src/main/java/org/linphone/activities/call/OutgoingCallActivity.kt
+++ /dev/null
@@ -1,236 +0,0 @@
-/*
- * Copyright (c) 2010-2020 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.linphone.activities.call
-
-import android.Manifest
-import android.animation.ValueAnimator
-import android.annotation.TargetApi
-import android.content.Intent
-import android.content.pm.PackageManager
-import android.os.Bundle
-import androidx.databinding.DataBindingUtil
-import androidx.lifecycle.ViewModelProvider
-import com.google.android.flexbox.FlexboxLayout
-import org.linphone.LinphoneApplication
-import org.linphone.LinphoneApplication.Companion.coreContext
-import org.linphone.R
-import org.linphone.activities.call.viewmodels.CallViewModel
-import org.linphone.activities.call.viewmodels.CallViewModelFactory
-import org.linphone.activities.call.viewmodels.ControlsViewModel
-import org.linphone.activities.main.MainActivity
-import org.linphone.core.Call
-import org.linphone.core.tools.Log
-import org.linphone.databinding.CallOutgoingActivityBinding
-import org.linphone.mediastream.Version
-import org.linphone.utils.PermissionHelper
-
-class OutgoingCallActivity : ProximitySensorActivity() {
- private lateinit var binding: CallOutgoingActivityBinding
- private lateinit var viewModel: CallViewModel
- private lateinit var controlsViewModel: ControlsViewModel
-
- // We have to use lateinit here because we need to compute the screen width first
- private lateinit var numpadAnimator: ValueAnimator
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- binding = DataBindingUtil.setContentView(this, R.layout.call_outgoing_activity)
- binding.lifecycleOwner = this
-
- val outgoingCall: Call? = findOutgoingCall()
- if (outgoingCall == null) {
- Log.e("[Outgoing Call Activity] Couldn't find call in state Outgoing")
- if (isTaskRoot) {
- // When resuming app from recent tasks make sure MainActivity will be launched if there is no call
- val intent = Intent()
- intent.setClass(this, MainActivity::class.java)
- intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
- startActivity(intent)
- } else {
- finish()
- }
- return
- }
-
- viewModel = ViewModelProvider(
- this,
- CallViewModelFactory(outgoingCall)
- )[CallViewModel::class.java]
- binding.viewModel = viewModel
-
- controlsViewModel = ViewModelProvider(this)[ControlsViewModel::class.java]
- binding.controlsViewModel = controlsViewModel
-
- viewModel.callEndedEvent.observe(
- this
- ) {
- it.consume {
- Log.i("[Outgoing Call Activity] Call ended, finish activity")
- finish()
- }
- }
-
- viewModel.callConnectedEvent.observe(
- this
- ) {
- it.consume {
- Log.i("[Outgoing Call Activity] Call connected, finish activity")
- finish()
- }
- }
-
- controlsViewModel.isSpeakerSelected.observe(
- this
- ) {
- enableProximitySensor(!it)
- }
-
- controlsViewModel.askAudioRecordPermissionEvent.observe(
- this
- ) {
- it.consume { permission ->
- requestPermissions(arrayOf(permission), 0)
- }
- }
-
- controlsViewModel.askCameraPermissionEvent.observe(
- this
- ) {
- it.consume { permission ->
- requestPermissions(arrayOf(permission), 0)
- }
- }
-
- controlsViewModel.toggleNumpadEvent.observe(
- this
- ) {
- it.consume { open ->
- if (this::numpadAnimator.isInitialized) {
- if (open) {
- numpadAnimator.start()
- } else {
- numpadAnimator.reverse()
- }
- }
- }
- }
-
- if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
- checkPermissions()
- }
- }
-
- override fun onStart() {
- super.onStart()
- initNumpadLayout()
- }
-
- override fun onStop() {
- numpadAnimator.end()
- super.onStop()
- }
-
- override fun onResume() {
- super.onResume()
-
- val outgoingCall: Call? = findOutgoingCall()
- if (outgoingCall == null) {
- Log.e("[Outgoing Call Activity] Couldn't find call in state Outgoing")
- if (isTaskRoot) {
- // When resuming app from recent tasks make sure MainActivity will be launched if there is no call
- val intent = Intent()
- intent.setClass(this, MainActivity::class.java)
- intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
- startActivity(intent)
- } else {
- finish()
- }
- }
- }
-
- @TargetApi(Version.API23_MARSHMALLOW_60)
- private fun checkPermissions() {
- val permissionsRequiredList = arrayListOf()
- if (!PermissionHelper.get().hasRecordAudioPermission()) {
- Log.i("[Outgoing Call Activity] Asking for RECORD_AUDIO permission")
- permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO)
- }
- if (viewModel.call.currentParams.isVideoEnabled && !PermissionHelper.get().hasCameraPermission()) {
- Log.i("[Outgoing Call Activity] Asking for CAMERA permission")
- permissionsRequiredList.add(Manifest.permission.CAMERA)
- }
- if (permissionsRequiredList.isNotEmpty()) {
- val permissionsRequired = arrayOfNulls(permissionsRequiredList.size)
- permissionsRequiredList.toArray(permissionsRequired)
- requestPermissions(permissionsRequired, 0)
- }
- }
-
- override fun onRequestPermissionsResult(
- requestCode: Int,
- permissions: Array,
- grantResults: IntArray
- ) {
- if (requestCode == 0) {
- for (i in permissions.indices) {
- when (permissions[i]) {
- Manifest.permission.RECORD_AUDIO -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
- Log.i("[Outgoing Call Activity] RECORD_AUDIO permission has been granted")
- controlsViewModel.updateMuteMicState()
- }
- Manifest.permission.CAMERA -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
- Log.i("[Outgoing Call Activity] CAMERA permission has been granted")
- coreContext.core.reloadVideoDevices()
- }
- }
- }
- }
- super.onRequestPermissionsResult(requestCode, permissions, grantResults)
- }
-
- private fun findOutgoingCall(): Call? {
- for (call in coreContext.core.calls) {
- if (call.state == Call.State.OutgoingInit ||
- call.state == Call.State.OutgoingProgress ||
- call.state == Call.State.OutgoingRinging ||
- call.state == Call.State.OutgoingEarlyMedia
- ) {
- return call
- }
- }
- return null
- }
-
- private fun initNumpadLayout() {
- val screenWidth = coreContext.screenWidth
- numpadAnimator = ValueAnimator.ofFloat(screenWidth, 0f).apply {
- addUpdateListener {
- val value = it.animatedValue as Float
- findViewById(R.id.numpad)?.translationX = -value
- duration = if (LinphoneApplication.corePreferences.enableAnimations) 500 else 0
- }
- }
- // Hide the numpad here as we can't set the translationX property on include tag in layout
- if (this::controlsViewModel.isInitialized && controlsViewModel.numpadVisibility.value == false) {
- findViewById(R.id.numpad)?.translationX = -screenWidth
- }
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/call/VideoZoomHelper.kt b/app/src/main/java/org/linphone/activities/call/VideoZoomHelper.kt
deleted file mode 100644
index 820bd3e95..000000000
--- a/app/src/main/java/org/linphone/activities/call/VideoZoomHelper.kt
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * Copyright (c) 2010-2020 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.linphone.activities.call
-
-import android.content.Context
-import android.view.GestureDetector
-import android.view.MotionEvent
-import android.view.ScaleGestureDetector
-import android.view.View
-import kotlin.math.max
-import kotlin.math.min
-import org.linphone.LinphoneApplication.Companion.coreContext
-import org.linphone.core.Call
-
-class VideoZoomHelper(context: Context, private var videoDisplayView: View) : GestureDetector.SimpleOnGestureListener() {
- private var scaleDetector: ScaleGestureDetector
-
- private var zoomFactor = 1f
- private var zoomCenterX = 0f
- private var zoomCenterY = 0f
-
- init {
- val gestureDetector = GestureDetector(context, this)
-
- scaleDetector = ScaleGestureDetector(
- context,
- object :
- ScaleGestureDetector.SimpleOnScaleGestureListener() {
- override fun onScale(detector: ScaleGestureDetector): Boolean {
- zoomFactor *= detector.scaleFactor
- // Don't let the object get too small or too large.
- // Zoom to make the video fill the screen vertically
- val portraitZoomFactor = videoDisplayView.height.toFloat() / (3 * videoDisplayView.width / 4)
- // Zoom to make the video fill the screen horizontally
- val landscapeZoomFactor = videoDisplayView.width.toFloat() / (3 * videoDisplayView.height / 4)
- zoomFactor = max(0.1f, min(zoomFactor, max(portraitZoomFactor, landscapeZoomFactor)))
-
- val currentCall: Call? = coreContext.core.currentCall
- if (currentCall != null) {
- currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY)
- return true
- }
-
- return false
- }
- }
- )
-
- videoDisplayView.setOnTouchListener { _, event ->
- val currentZoomFactor = zoomFactor
- scaleDetector.onTouchEvent(event)
-
- if (currentZoomFactor != zoomFactor) {
- // We did scale, prevent touch event from going further
- return@setOnTouchListener true
- }
-
- // If true, gesture detected, prevent touch event from going further
- // Otherwise it seems we didn't use event,
- // allow it to be dispatched somewhere else
- gestureDetector.onTouchEvent(event)
- }
- }
-
- override fun onScroll(
- e1: MotionEvent,
- e2: MotionEvent,
- distanceX: Float,
- distanceY: Float
- ): Boolean {
- val currentCall: Call? = coreContext.core.currentCall
- if (currentCall != null) {
- if (zoomFactor > 1) {
- // Video is zoomed, slide is used to change center of zoom
- if (distanceX > 0 && zoomCenterX < 1) {
- zoomCenterX += 0.01f
- } else if (distanceX < 0 && zoomCenterX > 0) {
- zoomCenterX -= 0.01f
- }
-
- if (distanceY < 0 && zoomCenterY < 1) {
- zoomCenterY += 0.01f
- } else if (distanceY > 0 && zoomCenterY > 0) {
- zoomCenterY -= 0.01f
- }
-
- if (zoomCenterX > 1) zoomCenterX = 1f
- if (zoomCenterX < 0) zoomCenterX = 0f
- if (zoomCenterY > 1) zoomCenterY = 1f
- if (zoomCenterY < 0) zoomCenterY = 0f
-
- currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY)
- return true
- }
- }
-
- return false
- }
-
- override fun onDoubleTap(e: MotionEvent?): Boolean {
- val currentCall: Call? = coreContext.core.currentCall
- if (currentCall != null) {
- if (zoomFactor == 1f) {
- // Zoom to make the video fill the screen vertically
- val portraitZoomFactor = videoDisplayView.height.toFloat() / (3 * videoDisplayView.width / 4)
- // Zoom to make the video fill the screen horizontally
- val landscapeZoomFactor = videoDisplayView.width.toFloat() / (3 * videoDisplayView.height / 4)
- zoomFactor = max(portraitZoomFactor, landscapeZoomFactor)
- } else {
- resetZoom()
- }
-
- currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY)
- return true
- }
-
- return false
- }
-
- private fun resetZoom() {
- zoomFactor = 1f
- zoomCenterY = 0.5f
- zoomCenterX = zoomCenterY
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/call/data/ConferenceParticipantData.kt b/app/src/main/java/org/linphone/activities/call/data/ConferenceParticipantData.kt
deleted file mode 100644
index 2af5a7a8a..000000000
--- a/app/src/main/java/org/linphone/activities/call/data/ConferenceParticipantData.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (c) 2010-2020 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.linphone.activities.call.data
-
-import androidx.lifecycle.MutableLiveData
-import org.linphone.contact.GenericContactData
-import org.linphone.core.Conference
-import org.linphone.core.Participant
-import org.linphone.core.tools.Log
-
-class ConferenceParticipantData(
- private val conference: Conference,
- val participant: Participant
-) :
- GenericContactData(participant.address) {
- private val isAdmin = MutableLiveData()
- val isMeAdmin = MutableLiveData()
-
- init {
- isAdmin.value = participant.isAdmin
- isMeAdmin.value = conference.me.isAdmin
- Log.i("[Conference Participant VM] Participant ${participant.address.asStringUriOnly()} is ${if (participant.isAdmin) "admin" else "not admin"}")
- Log.i("[Conference Participant VM] Me is ${if (conference.me.isAdmin) "admin" else "not admin"} and is ${if (conference.me.isFocus) "focus" else "not focus"}")
- }
-
- fun removeFromConference() {
- Log.i("[Conference Participant VM] Removing participant ${participant.address.asStringUriOnly()} from conference $conference")
- conference.removeParticipant(participant)
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/call/fragments/ControlsFragment.kt b/app/src/main/java/org/linphone/activities/call/fragments/ControlsFragment.kt
deleted file mode 100644
index 3765a4183..000000000
--- a/app/src/main/java/org/linphone/activities/call/fragments/ControlsFragment.kt
+++ /dev/null
@@ -1,323 +0,0 @@
-/*
- * Copyright (c) 2010-2020 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.linphone.activities.call.fragments
-
-import android.Manifest
-import android.animation.ValueAnimator
-import android.annotation.TargetApi
-import android.app.Dialog
-import android.content.Intent
-import android.content.pm.PackageManager.PERMISSION_GRANTED
-import android.os.Bundle
-import android.os.SystemClock
-import android.view.View
-import androidx.lifecycle.ViewModelProvider
-import com.google.android.flexbox.FlexboxLayout
-import org.linphone.LinphoneApplication.Companion.coreContext
-import org.linphone.LinphoneApplication.Companion.corePreferences
-import org.linphone.R
-import org.linphone.activities.GenericFragment
-import org.linphone.activities.call.viewmodels.CallsViewModel
-import org.linphone.activities.call.viewmodels.ConferenceViewModel
-import org.linphone.activities.call.viewmodels.ControlsViewModel
-import org.linphone.activities.call.viewmodels.SharedCallViewModel
-import org.linphone.activities.main.MainActivity
-import org.linphone.activities.main.viewmodels.DialogViewModel
-import org.linphone.core.Call
-import org.linphone.core.tools.Log
-import org.linphone.databinding.CallControlsFragmentBinding
-import org.linphone.mediastream.Version
-import org.linphone.utils.AppUtils
-import org.linphone.utils.DialogUtils
-import org.linphone.utils.Event
-import org.linphone.utils.PermissionHelper
-
-class ControlsFragment : GenericFragment() {
- private lateinit var callsViewModel: CallsViewModel
- private lateinit var controlsViewModel: ControlsViewModel
- private lateinit var conferenceViewModel: ConferenceViewModel
- private lateinit var sharedViewModel: SharedCallViewModel
-
- private var dialog: Dialog? = null
-
- override fun getLayoutId(): Int = R.layout.call_controls_fragment
-
- // We have to use lateinit here because we need to compute the screen width first
- private lateinit var numpadAnimator: ValueAnimator
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- binding.lifecycleOwner = viewLifecycleOwner
- useMaterialSharedAxisXForwardAnimation = false
-
- sharedViewModel = requireActivity().run {
- ViewModelProvider(this)[SharedCallViewModel::class.java]
- }
-
- callsViewModel = requireActivity().run {
- ViewModelProvider(this)[CallsViewModel::class.java]
- }
- binding.viewModel = callsViewModel
-
- controlsViewModel = requireActivity().run {
- ViewModelProvider(this)[ControlsViewModel::class.java]
- }
- binding.controlsViewModel = controlsViewModel
-
- conferenceViewModel = requireActivity().run {
- ViewModelProvider(this)[ConferenceViewModel::class.java]
- }
- binding.conferenceViewModel = conferenceViewModel
-
- callsViewModel.currentCallViewModel.observe(
- viewLifecycleOwner
- ) {
- if (it != null) {
- binding.activeCallTimer.base =
- SystemClock.elapsedRealtime() - (1000 * it.call.duration) // Linphone timestamps are in seconds
- binding.activeCallTimer.start()
- }
- }
-
- callsViewModel.noMoreCallEvent.observe(
- viewLifecycleOwner
- ) {
- it.consume {
- requireActivity().finish()
- }
- }
-
- callsViewModel.askWriteExternalStoragePermissionEvent.observe(
- viewLifecycleOwner
- ) {
- it.consume {
- if (!PermissionHelper.get().hasWriteExternalStoragePermission()) {
- Log.i("[Controls Fragment] Asking for WRITE_EXTERNAL_STORAGE permission")
- requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 2)
- }
- }
- }
-
- callsViewModel.callUpdateEvent.observe(
- viewLifecycleOwner
- ) {
- it.consume { call ->
- if (call.state == Call.State.StreamsRunning) {
- dialog?.dismiss()
- } else if (call.state == Call.State.UpdatedByRemote) {
- if (coreContext.core.isVideoCaptureEnabled || coreContext.core.isVideoDisplayEnabled) {
- if (call.currentParams.isVideoEnabled != call.remoteParams?.isVideoEnabled) {
- showCallVideoUpdateDialog(call)
- }
- } else {
- Log.w("[Controls Fragment] Video display & capture are disabled, don't show video dialog")
- }
- }
- }
- }
-
- controlsViewModel.chatClickedEvent.observe(
- viewLifecycleOwner
- ) {
- it.consume {
- val intent = Intent()
- intent.setClass(requireContext(), MainActivity::class.java)
- intent.putExtra("Chat", true)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- startActivity(intent)
- }
- }
-
- controlsViewModel.addCallClickedEvent.observe(
- viewLifecycleOwner
- ) {
- it.consume {
- val intent = Intent()
- intent.setClass(requireContext(), MainActivity::class.java)
- intent.putExtra("Dialer", true)
- intent.putExtra("Transfer", false)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- startActivity(intent)
- }
- }
-
- controlsViewModel.transferCallClickedEvent.observe(
- viewLifecycleOwner
- ) {
- it.consume {
- val intent = Intent()
- intent.setClass(requireContext(), MainActivity::class.java)
- intent.putExtra("Dialer", true)
- intent.putExtra("Transfer", true)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- startActivity(intent)
- }
- }
-
- controlsViewModel.askAudioRecordPermissionEvent.observe(
- viewLifecycleOwner
- ) {
- it.consume { permission ->
- Log.i("[Controls Fragment] Asking for $permission permission")
- requestPermissions(arrayOf(permission), 0)
- }
- }
-
- controlsViewModel.askCameraPermissionEvent.observe(
- viewLifecycleOwner
- ) {
- it.consume { permission ->
- Log.i("[Controls Fragment] Asking for $permission permission")
- requestPermissions(arrayOf(permission), 1)
- }
- }
-
- controlsViewModel.toggleNumpadEvent.observe(
- viewLifecycleOwner
- ) {
- it.consume { open ->
- if (this::numpadAnimator.isInitialized) {
- if (open) {
- numpadAnimator.start()
- } else {
- numpadAnimator.reverse()
- }
- }
- }
- }
-
- controlsViewModel.somethingClickedEvent.observe(
- viewLifecycleOwner
- ) {
- it.consume {
- sharedViewModel.resetHiddenInterfaceTimerInVideoCallEvent.value = Event(true)
- }
- }
-
- if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
- checkPermissions()
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- onBackPressedCallback.isEnabled = false
- }
-
- override fun onStart() {
- super.onStart()
- initNumpadLayout()
- }
-
- override fun onStop() {
- numpadAnimator.end()
- super.onStop()
- }
-
- override fun onRequestPermissionsResult(
- requestCode: Int,
- permissions: Array,
- grantResults: IntArray
- ) {
- if (requestCode == 0) {
- for (i in permissions.indices) {
- when (permissions[i]) {
- Manifest.permission.RECORD_AUDIO -> if (grantResults[i] == PERMISSION_GRANTED) {
- Log.i("[Controls Fragment] RECORD_AUDIO permission has been granted")
- controlsViewModel.updateMuteMicState()
- }
- Manifest.permission.CAMERA -> if (grantResults[i] == PERMISSION_GRANTED) {
- Log.i("[Controls Fragment] CAMERA permission has been granted")
- coreContext.core.reloadVideoDevices()
- }
- }
- }
- } else if (requestCode == 1) {
- if (grantResults.isNotEmpty() && grantResults[0] == PERMISSION_GRANTED) {
- Log.i("[Controls Fragment] CAMERA permission has been granted")
- coreContext.core.reloadVideoDevices()
- controlsViewModel.toggleVideo()
- }
- } else if (requestCode == 2 && grantResults.isNotEmpty() && grantResults[0] == PERMISSION_GRANTED) {
- callsViewModel.takeScreenshot()
- }
- super.onRequestPermissionsResult(requestCode, permissions, grantResults)
- }
-
- @TargetApi(Version.API23_MARSHMALLOW_60)
- private fun checkPermissions() {
- val permissionsRequiredList = arrayListOf()
-
- if (!PermissionHelper.get().hasRecordAudioPermission()) {
- Log.i("[Controls Fragment] Asking for RECORD_AUDIO permission")
- permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO)
- }
-
- if (coreContext.isVideoCallOrConferenceActive() && !PermissionHelper.get().hasCameraPermission()) {
- Log.i("[Controls Fragment] Asking for CAMERA permission")
- permissionsRequiredList.add(Manifest.permission.CAMERA)
- }
-
- if (permissionsRequiredList.isNotEmpty()) {
- val permissionsRequired = arrayOfNulls(permissionsRequiredList.size)
- permissionsRequiredList.toArray(permissionsRequired)
- requestPermissions(permissionsRequired, 0)
- }
- }
-
- private fun showCallVideoUpdateDialog(call: Call) {
- val viewModel = DialogViewModel(AppUtils.getString(R.string.call_video_update_requested_dialog))
- dialog = DialogUtils.getDialog(requireContext(), viewModel)
-
- viewModel.showCancelButton(
- {
- callsViewModel.answerCallVideoUpdateRequest(call, false)
- dialog?.dismiss()
- },
- getString(R.string.dialog_decline)
- )
-
- viewModel.showOkButton(
- {
- callsViewModel.answerCallVideoUpdateRequest(call, true)
- dialog?.dismiss()
- },
- getString(R.string.dialog_accept)
- )
-
- dialog?.show()
- }
-
- private fun initNumpadLayout() {
- val screenWidth = coreContext.screenWidth
- numpadAnimator = ValueAnimator.ofFloat(screenWidth, 0f).apply {
- addUpdateListener {
- val value = it.animatedValue as Float
- view?.findViewById(R.id.numpad)?.translationX = -value
- duration = if (corePreferences.enableAnimations) 500 else 0
- }
- }
- // Hide the numpad here as we can't set the translationX property on include tag in layout
- if (controlsViewModel.numpadVisibility.value == false) {
- view?.findViewById(R.id.numpad)?.translationX = -screenWidth
- }
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/call/fragments/VideoRenderingFragment.kt b/app/src/main/java/org/linphone/activities/call/fragments/VideoRenderingFragment.kt
deleted file mode 100644
index c7f980c52..000000000
--- a/app/src/main/java/org/linphone/activities/call/fragments/VideoRenderingFragment.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Copyright (c) 2010-2021 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.linphone.activities.call.fragments
-
-import android.os.Bundle
-import android.view.MotionEvent
-import android.view.View
-import androidx.lifecycle.ViewModelProvider
-import org.linphone.LinphoneApplication.Companion.coreContext
-import org.linphone.R
-import org.linphone.activities.GenericFragment
-import org.linphone.activities.call.VideoZoomHelper
-import org.linphone.activities.call.viewmodels.CallsViewModel
-import org.linphone.activities.call.viewmodels.ConferenceViewModel
-import org.linphone.activities.call.viewmodels.ControlsFadingViewModel
-import org.linphone.databinding.CallVideoFragmentBinding
-
-class VideoRenderingFragment : GenericFragment() {
- private lateinit var controlsFadingViewModel: ControlsFadingViewModel
- private lateinit var callsViewModel: CallsViewModel
- private lateinit var conferenceViewModel: ConferenceViewModel
-
- private var previewX: Float = 0f
- private var previewY: Float = 0f
- private lateinit var videoZoomHelper: VideoZoomHelper
-
- override fun getLayoutId(): Int = R.layout.call_video_fragment
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- binding.lifecycleOwner = this
-
- controlsFadingViewModel = requireActivity().run {
- ViewModelProvider(this)[ControlsFadingViewModel::class.java]
- }
- binding.controlsFadingViewModel = controlsFadingViewModel
-
- callsViewModel = requireActivity().run {
- ViewModelProvider(this)[CallsViewModel::class.java]
- }
-
- conferenceViewModel = requireActivity().run {
- ViewModelProvider(this)[ConferenceViewModel::class.java]
- }
- binding.conferenceViewModel = conferenceViewModel
-
- coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface
- coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface
-
- binding.setPreviewTouchListener { v, event ->
- when (event.action) {
- MotionEvent.ACTION_DOWN -> {
- previewX = v.x - event.rawX
- previewY = v.y - event.rawY
- }
- MotionEvent.ACTION_MOVE -> {
- v.animate()
- .x(event.rawX + previewX)
- .y(event.rawY + previewY)
- .setDuration(0)
- .start()
- }
- else -> {
- v.performClick()
- false
- }
- }
- true
- }
-
- videoZoomHelper = VideoZoomHelper(requireContext(), binding.remoteVideoSurface)
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/CallViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/CallViewModel.kt
deleted file mode 100644
index 59216ead9..000000000
--- a/app/src/main/java/org/linphone/activities/call/viewmodels/CallViewModel.kt
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (c) 2010-2020 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.linphone.activities.call.viewmodels
-
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
-import androidx.lifecycle.viewModelScope
-import java.util.*
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.linphone.LinphoneApplication.Companion.coreContext
-import org.linphone.compatibility.Compatibility
-import org.linphone.contact.GenericContactViewModel
-import org.linphone.core.Call
-import org.linphone.core.CallListenerStub
-import org.linphone.core.Factory
-import org.linphone.core.tools.Log
-import org.linphone.utils.Event
-import org.linphone.utils.FileUtils
-import org.linphone.utils.LinphoneUtils
-
-class CallViewModelFactory(private val call: Call) :
- ViewModelProvider.NewInstanceFactory() {
-
- @Suppress("UNCHECKED_CAST")
- override fun create(modelClass: Class): T {
- return CallViewModel(call) as T
- }
-}
-
-open class CallViewModel(val call: Call) : GenericContactViewModel(call.remoteAddress) {
- val address: String by lazy {
- LinphoneUtils.getDisplayableAddress(call.remoteAddress)
- }
-
- val isPaused = MutableLiveData()
-
- val isOutgoingEarlyMedia = MutableLiveData()
-
- val callEndedEvent: MutableLiveData> by lazy {
- MutableLiveData>()
- }
-
- val callConnectedEvent: MutableLiveData> by lazy {
- MutableLiveData>()
- }
-
- private var timer: Timer? = null
-
- private val listener = object : CallListenerStub() {
- override fun onStateChanged(call: Call, state: Call.State, message: String) {
- if (call != this@CallViewModel.call) return
-
- isPaused.value = state == Call.State.Paused
- isOutgoingEarlyMedia.value = state == Call.State.OutgoingEarlyMedia
-
- if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) {
- timer?.cancel()
- callEndedEvent.value = Event(true)
-
- if (state == Call.State.Error) {
- Log.e("[Call View Model] Error state reason is ${call.reason}")
- }
- } else if (call.state == Call.State.Connected) {
- callConnectedEvent.value = Event(true)
- } else if (call.state == Call.State.StreamsRunning) {
- // Stop call update timer once user has accepted or declined call update
- timer?.cancel()
- } else if (call.state == Call.State.UpdatedByRemote) {
- // User has 30 secs to accept or decline call update
- // Dialog to accept or decline is handled by CallsViewModel & ControlsFragment
- startTimer(call)
- }
- }
-
- override fun onSnapshotTaken(call: Call, filePath: String) {
- Log.i("[Call View Model] Snapshot taken, saved at $filePath")
- val content = Factory.instance().createContent()
- content.filePath = filePath
- content.type = "image"
- content.subtype = "jpeg"
- content.name = filePath.substring(filePath.indexOf("/") + 1)
-
- viewModelScope.launch {
- if (Compatibility.addImageToMediaStore(coreContext.context, content)) {
- Log.i("[Call View Model] Adding snapshot ${content.name} to Media Store terminated")
- } else {
- Log.e("[Call View Model] Something went wrong while copying file to Media Store...")
- }
- }
- }
- }
-
- init {
- call.addListener(listener)
-
- isPaused.value = call.state == Call.State.Paused
- isOutgoingEarlyMedia.value = call.state == Call.State.OutgoingEarlyMedia
- }
-
- override fun onCleared() {
- destroy()
- super.onCleared()
- }
-
- fun destroy() {
- call.removeListener(listener)
- }
-
- fun terminateCall() {
- coreContext.terminateCall(call)
- }
-
- fun pause() {
- call.pause()
- }
-
- fun resume() {
- call.resume()
- }
-
- fun takeScreenshot() {
- if (call.currentParams.isVideoEnabled) {
- val fileName = System.currentTimeMillis().toString() + ".jpeg"
- call.takeVideoSnapshot(FileUtils.getFileStoragePath(fileName).absolutePath)
- }
- }
-
- private fun startTimer(call: Call) {
- timer?.cancel()
-
- timer = Timer("Call update timeout")
- timer?.schedule(
- object : TimerTask() {
- override fun run() {
- // Decline call update
- viewModelScope.launch {
- withContext(Dispatchers.Main) {
- coreContext.answerCallVideoUpdateRequest(call, false)
- }
- }
- }
- },
- 30000
- )
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/CallsViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/CallsViewModel.kt
deleted file mode 100644
index e6165e651..000000000
--- a/app/src/main/java/org/linphone/activities/call/viewmodels/CallsViewModel.kt
+++ /dev/null
@@ -1,165 +0,0 @@
-/*
- * Copyright (c) 2010-2020 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.linphone.activities.call.viewmodels
-
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-import org.linphone.LinphoneApplication.Companion.coreContext
-import org.linphone.core.*
-import org.linphone.core.tools.Log
-import org.linphone.utils.Event
-import org.linphone.utils.PermissionHelper
-
-class CallsViewModel : ViewModel() {
- val currentCallViewModel = MutableLiveData()
-
- val noActiveCall = MutableLiveData()
-
- val callPausedByRemote = MutableLiveData()
-
- val pausedCalls = MutableLiveData>()
-
- val noMoreCallEvent: MutableLiveData> by lazy {
- MutableLiveData>()
- }
-
- val callUpdateEvent: MutableLiveData> by lazy {
- MutableLiveData>()
- }
-
- val askWriteExternalStoragePermissionEvent: MutableLiveData> by lazy {
- MutableLiveData>()
- }
-
- private val listener = object : CoreListenerStub() {
- override fun onCallStateChanged(core: Core, call: Call, state: Call.State, message: String) {
- Log.i("[Calls VM] Call state changed: $state")
- callPausedByRemote.value = (state == Call.State.PausedByRemote) and (call.conference == null)
-
- val currentCall = core.currentCall
- noActiveCall.value = currentCall == null
- if (currentCall == null) {
- currentCallViewModel.value?.destroy()
- } else if (currentCallViewModel.value?.call != currentCall) {
- val viewModel = CallViewModel(currentCall)
- currentCallViewModel.value = viewModel
- }
-
- if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) {
- if (core.callsNb == 0) {
- noMoreCallEvent.value = Event(true)
- } else {
- removeCallFromPausedListIfPresent(call)
- }
- } else if (state == Call.State.Paused) {
- addCallToPausedList(call)
- } else if (state == Call.State.Resuming) {
- removeCallFromPausedListIfPresent(call)
- } else if (call.state == Call.State.UpdatedByRemote) {
- // If the correspondent asks to turn on video while audio call,
- // defer update until user has chosen whether to accept it or not
- val remoteVideo = call.remoteParams?.isVideoEnabled ?: false
- val localVideo = call.currentParams.isVideoEnabled
- val autoAccept = call.core.videoActivationPolicy.automaticallyAccept
- if (remoteVideo && !localVideo && !autoAccept) {
- if (coreContext.core.isVideoCaptureEnabled || coreContext.core.isVideoDisplayEnabled) {
- call.deferUpdate()
- callUpdateEvent.value = Event(call)
- } else {
- coreContext.answerCallVideoUpdateRequest(call, false)
- }
- }
- } else if (state == Call.State.StreamsRunning) {
- callUpdateEvent.value = Event(call)
- }
- }
- }
-
- init {
- coreContext.core.addListener(listener)
-
- val currentCall = coreContext.core.currentCall
- noActiveCall.value = currentCall == null
- if (currentCall != null) {
- currentCallViewModel.value?.destroy()
-
- val viewModel = CallViewModel(currentCall)
- currentCallViewModel.value = viewModel
- }
-
- callPausedByRemote.value = currentCall?.state == Call.State.PausedByRemote
-
- for (call in coreContext.core.calls) {
- if (call.state == Call.State.Paused || call.state == Call.State.Pausing) {
- addCallToPausedList(call)
- }
- }
- }
-
- override fun onCleared() {
- coreContext.core.removeListener(listener)
-
- super.onCleared()
- }
-
- fun answerCallVideoUpdateRequest(call: Call, accept: Boolean) {
- coreContext.answerCallVideoUpdateRequest(call, accept)
- }
-
- fun takeScreenshot() {
- if (!PermissionHelper.get().hasWriteExternalStoragePermission()) {
- askWriteExternalStoragePermissionEvent.value = Event(true)
- } else {
- currentCallViewModel.value?.takeScreenshot()
- }
- }
-
- private fun addCallToPausedList(call: Call) {
- if (call.conference != null) return // Conference will be displayed as paused, no need to display the call as well
-
- val list = arrayListOf()
- list.addAll(pausedCalls.value.orEmpty())
-
- for (pausedCallViewModel in list) {
- if (pausedCallViewModel.call == call) {
- return
- }
- }
-
- val viewModel = CallViewModel(call)
- list.add(viewModel)
- pausedCalls.value = list
- }
-
- private fun removeCallFromPausedListIfPresent(call: Call) {
- val list = arrayListOf()
- list.addAll(pausedCalls.value.orEmpty())
-
- for (pausedCallViewModel in list) {
- if (pausedCallViewModel.call == call) {
- pausedCallViewModel.destroy()
- list.remove(pausedCallViewModel)
- break
- }
- }
-
- pausedCalls.value = list
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/ConferenceViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/ConferenceViewModel.kt
deleted file mode 100644
index 164a067bb..000000000
--- a/app/src/main/java/org/linphone/activities/call/viewmodels/ConferenceViewModel.kt
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (c) 2010-2020 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.linphone.activities.call.viewmodels
-
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-import org.linphone.LinphoneApplication.Companion.coreContext
-import org.linphone.activities.call.data.ConferenceParticipantData
-import org.linphone.core.*
-import org.linphone.core.tools.Log
-
-class ConferenceViewModel : ViewModel() {
- val isConferencePaused = MutableLiveData()
-
- val isMeConferenceFocus = MutableLiveData()
-
- val conferenceAddress = MutableLiveData()
-
- val conferenceParticipants = MutableLiveData>()
-
- val isInConference = MutableLiveData()
-
- private val conferenceListener = object : ConferenceListenerStub() {
- override fun onParticipantAdded(conference: Conference, participant: Participant) {
- if (conference.isMe(participant.address)) {
- Log.i("[Conference VM] Entered conference")
- isConferencePaused.value = false
- } else {
- Log.i("[Conference VM] Participant added")
- updateParticipantsList(conference)
- }
- }
-
- override fun onParticipantRemoved(conference: Conference, participant: Participant) {
- if (conference.isMe(participant.address)) {
- Log.i("[Conference VM] Left conference")
- isConferencePaused.value = true
- } else {
- Log.i("[Conference VM] Participant removed")
- updateParticipantsList(conference)
- }
- }
-
- override fun onParticipantAdminStatusChanged(
- conference: Conference,
- participant: Participant
- ) {
- Log.i("[Conference VM] Participant admin status changed")
- updateParticipantsList(conference)
- }
- }
-
- private val listener = object : CoreListenerStub() {
- override fun onConferenceStateChanged(
- core: Core,
- conference: Conference,
- state: Conference.State
- ) {
- Log.i("[Conference VM] Conference state changed: $state")
- isConferencePaused.value = !conference.isIn
-
- if (state == Conference.State.Instantiated) {
- conference.addListener(conferenceListener)
- } else if (state == Conference.State.Created) {
- updateParticipantsList(conference)
- isMeConferenceFocus.value = conference.me.isFocus
- conferenceAddress.value = conference.conferenceAddress
- } else if (state == Conference.State.Terminated || state == Conference.State.TerminationFailed) {
- isInConference.value = false
- conference.removeListener(conferenceListener)
- conferenceParticipants.value = arrayListOf()
- }
- }
- }
-
- init {
- coreContext.core.addListener(listener)
-
- isConferencePaused.value = coreContext.core.conference?.isIn != true
- isMeConferenceFocus.value = false
- conferenceParticipants.value = arrayListOf()
- isInConference.value = false
-
- val conference = coreContext.core.conference
- if (conference != null) {
- conference.addListener(conferenceListener)
- isMeConferenceFocus.value = conference.me.isFocus
- updateParticipantsList(conference)
- }
- }
-
- override fun onCleared() {
- coreContext.core.removeListener(listener)
-
- super.onCleared()
- }
-
- fun pauseConference() {
- val defaultProxyConfig = coreContext.core.defaultProxyConfig
- val localAddress = defaultProxyConfig?.identityAddress
- val participants = arrayOf()
- val remoteConference = coreContext.core.searchConference(null, localAddress, conferenceAddress.value, participants)
- val localConference = coreContext.core.searchConference(null, conferenceAddress.value, conferenceAddress.value, participants)
- val conference = remoteConference ?: localConference
-
- if (conference != null) {
- Log.i("[Conference VM] Leaving conference with address ${conferenceAddress.value?.asStringUriOnly()} temporarily")
- conference.leave()
- } else {
- Log.w("[Conference VM] Unable to find conference with address ${conferenceAddress.value?.asStringUriOnly()}")
- }
- }
-
- fun resumeConference() {
- val defaultProxyConfig = coreContext.core.defaultProxyConfig
- val localAddress = defaultProxyConfig?.identityAddress
- val participants = arrayOf()
- val remoteConference = coreContext.core.searchConference(null, localAddress, conferenceAddress.value, participants)
- val localConference = coreContext.core.searchConference(null, conferenceAddress.value, conferenceAddress.value, participants)
- val conference = remoteConference ?: localConference
-
- if (conference != null) {
- Log.i("[Conference VM] Entering again conference with address ${conferenceAddress.value?.asStringUriOnly()}")
- conference.enter()
- } else {
- Log.w("[Conference VM] Unable to find conference with address ${conferenceAddress.value?.asStringUriOnly()}")
- }
- }
-
- private fun updateParticipantsList(conference: Conference) {
- val participants = arrayListOf()
- for (participant in conference.participantList) {
- Log.i("[Conference VM] Participant found: ${participant.address.asStringUriOnly()}")
- val viewModel = ConferenceParticipantData(conference, participant)
- participants.add(viewModel)
- }
- conferenceParticipants.value = participants
- isInConference.value = participants.isNotEmpty()
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsFadingViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsFadingViewModel.kt
deleted file mode 100644
index 1d231a8db..000000000
--- a/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsFadingViewModel.kt
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Copyright (c) 2010-2020 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.linphone.activities.call.viewmodels
-
-import androidx.lifecycle.MediatorLiveData
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import java.util.*
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.linphone.LinphoneApplication.Companion.coreContext
-import org.linphone.core.AudioDevice
-import org.linphone.core.Call
-import org.linphone.core.Core
-import org.linphone.core.CoreListenerStub
-import org.linphone.core.tools.Log
-
-class ControlsFadingViewModel : ViewModel() {
- val areControlsHidden = MutableLiveData()
-
- val isVideoPreviewHidden = MutableLiveData()
- val isVideoPreviewResizedForPip = MutableLiveData()
-
- val videoEnabled = MutableLiveData()
-
- val proximitySensorEnabled: MediatorLiveData = MediatorLiveData()
-
- private val nonEarpieceOutputAudioDevice = MutableLiveData()
-
- private var timer: Timer? = null
-
- private var disabled: Boolean = false
-
- private val listener = object : CoreListenerStub() {
- override fun onCallStateChanged(
- core: Core,
- call: Call,
- state: Call.State,
- message: String
- ) {
- if (state == Call.State.StreamsRunning || state == Call.State.Updating || state == Call.State.UpdatedByRemote) {
- val isVideoCall = coreContext.isVideoCallOrConferenceActive()
- Log.i("[Controls Fading] Call is in state $state, video is ${if (isVideoCall) "enabled" else "disabled"}")
- if (isVideoCall) {
- videoEnabled.value = true
- startTimer()
- } else {
- videoEnabled.value = false
- stopTimer()
- }
- }
- }
-
- override fun onAudioDeviceChanged(core: Core, audioDevice: AudioDevice) {
- if (audioDevice.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) {
- Log.i("[Controls Fading] Output audio device changed to: ${audioDevice.id}")
- nonEarpieceOutputAudioDevice.value = audioDevice.type != AudioDevice.Type.Earpiece
- }
- }
- }
-
- init {
- coreContext.core.addListener(listener)
-
- areControlsHidden.value = false
- isVideoPreviewHidden.value = false
- isVideoPreviewResizedForPip.value = false
- nonEarpieceOutputAudioDevice.value = coreContext.core.outputAudioDevice?.type != AudioDevice.Type.Earpiece
-
- val isVideoCall = coreContext.isVideoCallOrConferenceActive()
- videoEnabled.value = isVideoCall
- if (isVideoCall) {
- startTimer()
- }
-
- proximitySensorEnabled.value = shouldEnableProximitySensor()
- proximitySensorEnabled.addSource(videoEnabled) {
- proximitySensorEnabled.value = shouldEnableProximitySensor()
- }
- proximitySensorEnabled.addSource(nonEarpieceOutputAudioDevice) {
- proximitySensorEnabled.value = shouldEnableProximitySensor()
- }
- }
-
- override fun onCleared() {
- coreContext.core.removeListener(listener)
- stopTimer()
-
- super.onCleared()
- }
-
- fun showMomentarily() {
- stopTimer()
- startTimer()
- }
-
- fun disable(disable: Boolean) {
- disabled = disable
- if (disabled) {
- stopTimer()
- } else {
- startTimer()
- }
- }
-
- private fun shouldEnableProximitySensor(): Boolean {
- return !(videoEnabled.value ?: false) && !(nonEarpieceOutputAudioDevice.value ?: false)
- }
-
- private fun stopTimer() {
- timer?.cancel()
-
- areControlsHidden.value = false
- }
-
- private fun startTimer() {
- timer?.cancel()
- if (disabled) return
-
- timer = Timer("Hide UI controls scheduler")
- timer?.schedule(
- object : TimerTask() {
- override fun run() {
- viewModelScope.launch {
- withContext(Dispatchers.Main) {
- val videoEnabled = coreContext.isVideoCallOrConferenceActive()
- areControlsHidden.postValue(videoEnabled)
- }
- }
- }
- },
- 3000
- )
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsViewModel.kt
deleted file mode 100644
index 094cd64c2..000000000
--- a/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsViewModel.kt
+++ /dev/null
@@ -1,484 +0,0 @@
-/*
- * Copyright (c) 2010-2020 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.linphone.activities.call.viewmodels
-
-import android.Manifest
-import android.animation.ValueAnimator
-import android.content.Context
-import android.os.Vibrator
-import android.view.animation.LinearInterpolator
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import kotlin.math.max
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import org.linphone.LinphoneApplication.Companion.coreContext
-import org.linphone.LinphoneApplication.Companion.corePreferences
-import org.linphone.R
-import org.linphone.activities.main.dialer.NumpadDigitListener
-import org.linphone.compatibility.Compatibility
-import org.linphone.core.*
-import org.linphone.core.tools.Log
-import org.linphone.utils.*
-import org.linphone.utils.Event
-
-class ControlsViewModel : ViewModel() {
- val isMicrophoneMuted = MutableLiveData()
-
- val isMuteMicrophoneEnabled = MutableLiveData()
-
- val isSpeakerSelected = MutableLiveData()
-
- val isBluetoothHeadsetSelected = MutableLiveData()
-
- val isVideoAvailable = MutableLiveData()
-
- val isVideoEnabled = MutableLiveData()
-
- val isVideoUpdateInProgress = MutableLiveData()
-
- val showSwitchCamera = MutableLiveData()
-
- val isPauseEnabled = MutableLiveData()
-
- val isRecording = MutableLiveData()
-
- val isConferencingAvailable = MutableLiveData()
-
- val unreadMessagesCount = MutableLiveData()
-
- val numpadVisibility = MutableLiveData()
-
- val optionsVisibility = MutableLiveData()
-
- val audioRoutesSelected = MutableLiveData()
-
- val audioRoutesEnabled = MutableLiveData()
-
- val takeScreenshotEnabled = MutableLiveData()
-
- val chatClickedEvent: MutableLiveData> by lazy {
- MutableLiveData>()
- }
-
- val addCallClickedEvent: MutableLiveData> by lazy {
- MutableLiveData>()
- }
-
- val transferCallClickedEvent: MutableLiveData> by lazy {
- MutableLiveData>()
- }
-
- val askAudioRecordPermissionEvent: MutableLiveData> by lazy {
- MutableLiveData>()
- }
-
- val askCameraPermissionEvent: MutableLiveData> by lazy {
- MutableLiveData>()
- }
-
- val somethingClickedEvent = MutableLiveData>()
-
- val chatAllowed = !corePreferences.disableChat
-
- private val vibrator = coreContext.context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
-
- val chatUnreadCountTranslateY = MutableLiveData()
-
- val optionsMenuTranslateY = MutableLiveData()
-
- val audioRoutesMenuTranslateY = MutableLiveData()
-
- val toggleNumpadEvent: MutableLiveData> by lazy {
- MutableLiveData>()
- }
-
- private val bounceAnimator: ValueAnimator by lazy {
- ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.tabs_fragment_unread_count_bounce_offset), 0f).apply {
- addUpdateListener {
- val value = it.animatedValue as Float
- chatUnreadCountTranslateY.value = -value
- }
- interpolator = LinearInterpolator()
- duration = 250
- repeatMode = ValueAnimator.REVERSE
- repeatCount = ValueAnimator.INFINITE
- }
- }
-
- private val optionsMenuAnimator: ValueAnimator by lazy {
- ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.call_options_menu_translate_y), 0f).apply {
- addUpdateListener {
- val value = it.animatedValue as Float
- optionsMenuTranslateY.value = value
- }
- duration = if (corePreferences.enableAnimations) 500 else 0
- }
- }
-
- private val audioRoutesMenuAnimator: ValueAnimator by lazy {
- ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.call_audio_routes_menu_translate_y), 0f).apply {
- addUpdateListener {
- val value = it.animatedValue as Float
- audioRoutesMenuTranslateY.value = value
- }
- duration = if (corePreferences.enableAnimations) 500 else 0
- }
- }
-
- val onKeyClick: NumpadDigitListener = object : NumpadDigitListener {
- override fun handleClick(key: Char) {
- coreContext.core.playDtmf(key, 1)
- somethingClickedEvent.value = Event(true)
- coreContext.core.currentCall?.sendDtmf(key)
-
- if (vibrator.hasVibrator() && corePreferences.dtmfKeypadVibration) {
- Compatibility.eventVibration(vibrator)
- }
- }
-
- override fun handleLongClick(key: Char): Boolean {
- return true
- }
- }
-
- private val listener: CoreListenerStub = object : CoreListenerStub() {
- override fun onMessageReceived(core: Core, chatRoom: ChatRoom, message: ChatMessage) {
- updateUnreadChatCount()
- }
-
- override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) {
- updateUnreadChatCount()
- }
-
- override fun onCallStateChanged(
- core: Core,
- call: Call,
- state: Call.State,
- message: String
- ) {
- if (state == Call.State.StreamsRunning) {
- isVideoUpdateInProgress.value = false
- }
-
- if (coreContext.isVideoCallOrConferenceActive() && !PermissionHelper.get().hasCameraPermission()) {
- askCameraPermissionEvent.value = Event(Manifest.permission.CAMERA)
- }
-
- updateUI()
- }
-
- override fun onAudioDeviceChanged(core: Core, audioDevice: AudioDevice) {
- Log.i("[Call] Audio device changed: ${audioDevice.deviceName}")
- updateSpeakerState()
- updateBluetoothHeadsetState()
- }
-
- override fun onAudioDevicesListUpdated(core: Core) {
- Log.i("[Call] Audio devices list updated")
- val wasBluetoothPreviouslyAvailable = audioRoutesEnabled.value == true
- updateAudioRoutesState()
-
- if (AudioRouteUtils.isHeadsetAudioRouteAvailable()) {
- AudioRouteUtils.routeAudioToHeadset()
- } else if (!wasBluetoothPreviouslyAvailable && corePreferences.routeAudioToBluetoothIfAvailable) {
- // Only attempt to route audio to bluetooth automatically when bluetooth device is connected
- if (AudioRouteUtils.isBluetoothAudioRouteAvailable()) {
- AudioRouteUtils.routeAudioToBluetooth()
- }
- }
- }
- }
-
- init {
- coreContext.core.addListener(listener)
- val currentCall = coreContext.core.currentCall
-
- updateMuteMicState()
- updateAudioRelated()
- updateUnreadChatCount()
-
- numpadVisibility.value = false
- optionsVisibility.value = false
- audioRoutesSelected.value = false
-
- isRecording.value = currentCall?.isRecording
- isVideoUpdateInProgress.value = false
- showSwitchCamera.value = coreContext.showSwitchCameraButton()
-
- chatUnreadCountTranslateY.value = 0f
- optionsMenuTranslateY.value = AppUtils.getDimension(R.dimen.call_options_menu_translate_y)
- audioRoutesMenuTranslateY.value = AppUtils.getDimension(R.dimen.call_audio_routes_menu_translate_y)
-
- takeScreenshotEnabled.value = corePreferences.showScreenshotButton
-
- updateUI()
- if (corePreferences.enableAnimations) bounceAnimator.start()
- }
-
- override fun onCleared() {
- if (corePreferences.enableAnimations) bounceAnimator.end()
- optionsMenuAnimator.end()
- audioRoutesMenuAnimator.end()
- coreContext.core.removeListener(listener)
- super.onCleared()
- }
-
- fun updateUnreadChatCount() {
- unreadMessagesCount.value = coreContext.core.unreadChatMessageCountFromActiveLocals
- }
-
- fun toggleMuteMicrophone() {
- if (!PermissionHelper.get().hasRecordAudioPermission()) {
- askAudioRecordPermissionEvent.value = Event(Manifest.permission.RECORD_AUDIO)
- return
- }
-
- somethingClickedEvent.value = Event(true)
- val micEnabled = coreContext.core.isMicEnabled
- coreContext.core.isMicEnabled = !micEnabled
- updateMuteMicState()
- }
-
- fun toggleSpeaker() {
- somethingClickedEvent.value = Event(true)
- if (AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed()) {
- forceEarpieceAudioRoute()
- } else {
- forceSpeakerAudioRoute()
- }
- }
-
- fun switchCamera() {
- somethingClickedEvent.value = Event(true)
- coreContext.switchCamera()
- }
-
- fun terminateCall() {
- val core = coreContext.core
- when {
- core.currentCall != null -> core.currentCall?.terminate()
- core.conference?.isIn == true -> core.terminateConference()
- else -> core.terminateAllCalls()
- }
- }
-
- fun toggleVideo() {
- if (!PermissionHelper.get().hasCameraPermission()) {
- askCameraPermissionEvent.value = Event(Manifest.permission.CAMERA)
- return
- }
-
- val core = coreContext.core
- val currentCall = core.currentCall
- val conference = core.conference
-
- if (conference != null && conference.isIn) {
- val params = core.createConferenceParams()
- val videoEnabled = conference.currentParams.isVideoEnabled
- params.isVideoEnabled = !videoEnabled
- Log.i("[Controls VM] Conference current param for video is $videoEnabled")
- conference.updateParams(params)
- } else if (currentCall != null) {
- val state = currentCall.state
- if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error)
- return
-
- isVideoUpdateInProgress.value = true
- val params = core.createCallParams(currentCall)
- params?.isVideoEnabled = !currentCall.currentParams.isVideoEnabled
- currentCall.update(params)
- }
- }
-
- fun toggleOptionsMenu() {
- somethingClickedEvent.value = Event(true)
- optionsVisibility.value = optionsVisibility.value != true
- if (optionsVisibility.value == true) {
- optionsMenuAnimator.start()
- } else {
- optionsMenuAnimator.reverse()
- }
- }
-
- fun toggleNumpadVisibility() {
- somethingClickedEvent.value = Event(true)
- numpadVisibility.value = numpadVisibility.value != true
- toggleNumpadEvent.value = Event(numpadVisibility.value ?: true)
- }
-
- fun toggleRoutesMenu() {
- somethingClickedEvent.value = Event(true)
- audioRoutesSelected.value = audioRoutesSelected.value != true
- if (audioRoutesSelected.value == true) {
- audioRoutesMenuAnimator.start()
- } else {
- audioRoutesMenuAnimator.reverse()
- }
- }
-
- fun toggleRecording(closeMenu: Boolean) {
- somethingClickedEvent.value = Event(true)
-
- val core = coreContext.core
- val currentCall = core.currentCall
- val conference = core.conference
-
- when {
- currentCall != null -> {
- if (currentCall.isRecording) {
- currentCall.stopRecording()
- } else {
- currentCall.startRecording()
- }
- isRecording.value = currentCall.isRecording
- }
- conference != null -> {
- val path = LinphoneUtils.getRecordingFilePathForConference()
- if (conference.isRecording) {
- conference.stopRecording()
- } else {
- conference.startRecording(path)
- }
- isRecording.value = conference.isRecording
- }
- else -> {
- isRecording.value = false
- }
- }
-
- if (closeMenu) toggleOptionsMenu()
- }
-
- fun onChatClicked() {
- chatClickedEvent.value = Event(true)
- }
-
- fun onAddCallClicked() {
- addCallClickedEvent.value = Event(true)
- toggleOptionsMenu()
- }
-
- fun onTransferCallClicked() {
- transferCallClickedEvent.value = Event(true)
- toggleOptionsMenu()
- }
-
- fun startConference() {
- somethingClickedEvent.value = Event(true)
-
- val core = coreContext.core
- val currentCallVideoEnabled = core.currentCall?.currentParams?.isVideoEnabled ?: false
-
- val params = core.createConferenceParams()
- params.isVideoEnabled = currentCallVideoEnabled
- Log.i("[Call] Setting videoEnabled to [$currentCallVideoEnabled] in conference params")
-
- val conference = core.conference ?: core.createConferenceWithParams(params)
- conference?.addParticipants(core.calls)
-
- toggleOptionsMenu()
- }
-
- fun forceEarpieceAudioRoute() {
- somethingClickedEvent.value = Event(true)
- if (AudioRouteUtils.isHeadsetAudioRouteAvailable()) {
- Log.i("[Call] Headset found, route audio to it instead of earpiece")
- AudioRouteUtils.routeAudioToHeadset()
- } else {
- AudioRouteUtils.routeAudioToEarpiece()
- }
- }
-
- fun forceSpeakerAudioRoute() {
- somethingClickedEvent.value = Event(true)
- AudioRouteUtils.routeAudioToSpeaker()
- }
-
- fun forceBluetoothAudioRoute() {
- somethingClickedEvent.value = Event(true)
- AudioRouteUtils.routeAudioToBluetooth()
- }
-
- fun updateMuteMicState() {
- isMicrophoneMuted.value = !PermissionHelper.get().hasRecordAudioPermission() || !coreContext.core.isMicEnabled
- isMuteMicrophoneEnabled.value = coreContext.core.currentCall != null || coreContext.core.conference?.isIn == true
- }
-
- private fun updateAudioRelated() {
- updateSpeakerState()
- updateBluetoothHeadsetState()
- updateAudioRoutesState()
- }
-
- private fun updateUI() {
- val currentCall = coreContext.core.currentCall
- updateVideoAvailable()
- updateVideoEnabled()
- isPauseEnabled.value = currentCall != null && !currentCall.mediaInProgress()
- isMuteMicrophoneEnabled.value = currentCall != null || coreContext.core.conference?.isIn == true
- updateConferenceState()
-
- // Check periodically until mediaInProgress is false
- if (currentCall != null && currentCall.mediaInProgress()) {
- viewModelScope.launch {
- delay(1000)
- updateUI()
- }
- }
- }
-
- private fun updateSpeakerState() {
- isSpeakerSelected.value = AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed()
- }
-
- private fun updateAudioRoutesState() {
- val bluetoothDeviceAvailable = AudioRouteUtils.isBluetoothAudioRouteAvailable()
- audioRoutesEnabled.value = bluetoothDeviceAvailable
- if (!bluetoothDeviceAvailable) {
- audioRoutesSelected.value = false
- }
- }
-
- private fun updateBluetoothHeadsetState() {
- isBluetoothHeadsetSelected.value = AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed()
- }
-
- private fun updateVideoAvailable() {
- val core = coreContext.core
- val currentCall = core.currentCall
- isVideoAvailable.value = (core.isVideoCaptureEnabled || core.isVideoPreviewEnabled) &&
- (
- (currentCall != null && !currentCall.mediaInProgress()) ||
- core.conference?.isIn == true
- )
- }
-
- private fun updateVideoEnabled() {
- val enabled = coreContext.isVideoCallOrConferenceActive()
- isVideoEnabled.value = enabled
- }
-
- private fun updateConferenceState() {
- val core = coreContext.core
- isConferencingAvailable.value = core.callsNb > max(1, core.conference?.participantCount ?: 0) && !core.soundResourcesLocked()
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/IncomingCallViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/IncomingCallViewModel.kt
deleted file mode 100644
index f1dc0bfb6..000000000
--- a/app/src/main/java/org/linphone/activities/call/viewmodels/IncomingCallViewModel.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (c) 2010-2020 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.linphone.activities.call.viewmodels
-
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.ViewModelProvider
-import org.linphone.LinphoneApplication.Companion.coreContext
-import org.linphone.LinphoneApplication.Companion.corePreferences
-import org.linphone.core.*
-import org.linphone.utils.Event
-
-class IncomingCallViewModelFactory(private val call: Call) :
- ViewModelProvider.NewInstanceFactory() {
-
- @Suppress("UNCHECKED_CAST")
- override fun create(modelClass: Class): T {
- return IncomingCallViewModel(call) as T
- }
-}
-
-class IncomingCallViewModel(call: Call) : CallViewModel(call) {
- val screenLocked = MutableLiveData()
-
- val earlyMediaVideoEnabled = MutableLiveData()
-
- val inviteWithVideo = MutableLiveData()
-
- private val listener = object : CoreListenerStub() {
- override fun onCallStateChanged(
- core: Core,
- call: Call,
- state: Call.State,
- message: String
- ) {
- if (core.callsNb == 0) {
- callEndedEvent.value = Event(true)
- }
- }
- }
-
- init {
- coreContext.core.addListener(listener)
-
- screenLocked.value = false
- inviteWithVideo.value = call.remoteParams?.isVideoEnabled == true && coreContext.core.videoActivationPolicy.automaticallyAccept
- earlyMediaVideoEnabled.value = corePreferences.acceptEarlyMedia &&
- call.state == Call.State.IncomingEarlyMedia &&
- call.currentParams.isVideoEnabled
- }
-
- override fun onCleared() {
- coreContext.core.removeListener(listener)
- super.onCleared()
- }
-
- fun answer(doAction: Boolean) {
- if (doAction) coreContext.answerCall(call)
- }
-
- fun decline(doAction: Boolean) {
- if (doAction) coreContext.declineCall(call)
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/call/views/AnswerDeclineIncomingCallButtons.kt b/app/src/main/java/org/linphone/activities/call/views/AnswerDeclineIncomingCallButtons.kt
deleted file mode 100644
index b030ea7a8..000000000
--- a/app/src/main/java/org/linphone/activities/call/views/AnswerDeclineIncomingCallButtons.kt
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright (c) 2010-2020 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.linphone.activities.call.views
-
-import android.animation.AnimatorSet
-import android.animation.ObjectAnimator
-import android.content.Context
-import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.view.MotionEvent
-import android.view.View
-import android.view.View.OnTouchListener
-import android.view.animation.LinearInterpolator
-import android.widget.LinearLayout
-import androidx.databinding.DataBindingUtil
-import org.linphone.LinphoneApplication.Companion.corePreferences
-import org.linphone.R
-import org.linphone.activities.call.viewmodels.IncomingCallViewModel
-import org.linphone.core.tools.Log
-import org.linphone.databinding.CallIncomingAnswerDeclineButtonsBinding
-
-class AnswerDeclineIncomingCallButtons : LinearLayout {
- private lateinit var binding: CallIncomingAnswerDeclineButtonsBinding
- private var mBegin = false
- private var mDeclineX = 0f
- private var mAnswerX = 0f
- private var mOldSize = 0f
-
- private val mAnswerTouchListener = OnTouchListener { view, motionEvent ->
- val curX: Float
-
- when (motionEvent.action) {
- MotionEvent.ACTION_DOWN -> {
- binding.declineButton.visibility = View.GONE
- mAnswerX = motionEvent.x - view.width
- mBegin = true
- mOldSize = 0f
- }
- MotionEvent.ACTION_MOVE -> {
- curX = motionEvent.x - view.width
- view.scrollBy((mAnswerX - curX).toInt(), view.scrollY)
- mOldSize -= mAnswerX - curX
- mAnswerX = curX
- if (mOldSize < -25) mBegin = false
- if (curX < (width / 4) - view.width && !mBegin) {
- binding.viewModel?.answer(true)
- }
- }
- MotionEvent.ACTION_UP -> {
- binding.declineButton.visibility = View.VISIBLE
- view.scrollTo(0, view.scrollY)
- }
- }
- true
- }
- private val mDeclineTouchListener = OnTouchListener { view, motionEvent ->
- val curX: Float
-
- when (motionEvent.action) {
- MotionEvent.ACTION_DOWN -> {
- binding.answerButton.visibility = View.GONE
- mDeclineX = motionEvent.x
- }
- MotionEvent.ACTION_MOVE -> {
- curX = motionEvent.x
- view.scrollBy((mDeclineX - curX).toInt(), view.scrollY)
- mDeclineX = curX
- if (curX > 3 * width / 4) {
- binding.viewModel?.decline(true)
- }
- }
- MotionEvent.ACTION_UP -> {
- binding.answerButton.visibility = View.VISIBLE
- view.scrollTo(0, view.scrollY)
- }
- }
- true
- }
-
- constructor(context: Context) : super(context) {
- init(context)
- }
-
- constructor(context: Context, attrs: AttributeSet) : super(
- context,
- attrs
- ) {
- init(context)
- }
-
- constructor(
- context: Context,
- attrs: AttributeSet,
- defStyleAttr: Int
- ) : super(context, attrs, defStyleAttr) {
- init(context)
- }
-
- fun setViewModel(viewModel: IncomingCallViewModel) {
- binding.viewModel = viewModel
-
- updateSlideMode()
- }
-
- private fun init(context: Context) {
- binding = DataBindingUtil.inflate(
- LayoutInflater.from(context), R.layout.call_incoming_answer_decline_buttons, this, true
- )
-
- updateSlideMode()
- configureAnimation()
- }
-
- private fun updateSlideMode() {
- val slideMode = binding.viewModel?.screenLocked?.value == true
- Log.i("[Call Incoming Decline Button] Slide mode is $slideMode")
- if (slideMode) {
- binding.answerButton.setOnTouchListener(mAnswerTouchListener)
- binding.declineButton.setOnTouchListener(mDeclineTouchListener)
- }
- }
-
- private fun configureAnimation() {
- if (!corePreferences.enableAnimations) return
-
- val accept1 = ObjectAnimator.ofFloat(binding.arrowAccept1, "alpha", 1f, 0.6f, 0.4f, 1f).apply {
- repeatCount = ObjectAnimator.INFINITE
- repeatMode = ObjectAnimator.RESTART
- }
-
- val accept2 = ObjectAnimator.ofFloat(binding.arrowAccept2, "alpha", 0.6f, 1f, 0.4f, 0.6f).apply {
- repeatCount = ObjectAnimator.INFINITE
- repeatMode = ObjectAnimator.RESTART
- }
-
- val accept3 = ObjectAnimator.ofFloat(binding.arrowAccept3, "alpha", 0.4f, 0.6f, 1f, 0.4f).apply {
- repeatCount = ObjectAnimator.INFINITE
- repeatMode = ObjectAnimator.RESTART
- }
-
- val hangup1 = ObjectAnimator.ofFloat(binding.arrowHangup1, "alpha", 1f, 0.6f, 0.4f, 1f).apply {
- repeatCount = ObjectAnimator.INFINITE
- repeatMode = ObjectAnimator.RESTART
- }
-
- val hangup2 = ObjectAnimator.ofFloat(binding.arrowHangup2, "alpha", 0.6f, 1f, 0.4f, 0.6f).apply {
- repeatCount = ObjectAnimator.INFINITE
- repeatMode = ObjectAnimator.RESTART
- }
-
- val hangup3 = ObjectAnimator.ofFloat(binding.arrowHangup3, "alpha", 0.4f, 0.6f, 1f, 0.4f).apply {
- repeatCount = ObjectAnimator.INFINITE
- repeatMode = ObjectAnimator.RESTART
- }
-
- AnimatorSet().apply {
- duration = 2000
- interpolator = LinearInterpolator()
- playTogether(accept1, accept2, accept3, hangup1, hangup2, hangup3)
- start()
- }
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/call/views/ConferenceParticipantView.kt b/app/src/main/java/org/linphone/activities/call/views/ConferenceParticipantView.kt
deleted file mode 100644
index 16499951f..000000000
--- a/app/src/main/java/org/linphone/activities/call/views/ConferenceParticipantView.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (c) 2010-2020 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.linphone.activities.call.views
-
-import android.content.Context
-import android.os.SystemClock
-import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.widget.LinearLayout
-import androidx.databinding.DataBindingUtil
-import org.linphone.R
-import org.linphone.activities.call.data.ConferenceParticipantData
-import org.linphone.core.tools.Log
-import org.linphone.databinding.CallConferenceParticipantBinding
-
-class ConferenceParticipantView : LinearLayout {
- private lateinit var binding: CallConferenceParticipantBinding
-
- constructor(context: Context) : super(context) {
- init(context)
- }
-
- constructor(context: Context, attrs: AttributeSet) : super(
- context,
- attrs
- ) {
- init(context)
- }
-
- constructor(
- context: Context,
- attrs: AttributeSet,
- defStyleAttr: Int
- ) : super(context, attrs, defStyleAttr) {
- init(context)
- }
-
- fun init(context: Context) {
- binding = DataBindingUtil.inflate(
- LayoutInflater.from(context), R.layout.call_conference_participant, this, true
- )
- }
-
- fun setData(data: ConferenceParticipantData) {
- binding.data = data
-
- val currentTimeSecs = System.currentTimeMillis()
- val participantTime = data.participant.creationTime * 1000 // Linphone timestamps are in seconds
- val diff = currentTimeSecs - participantTime
- Log.i("[Conference Participant] Participant joined conference at $participantTime == ${diff / 1000} seconds ago.")
- binding.callTimer.base = SystemClock.elapsedRealtime() - diff
- binding.callTimer.start()
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/call/views/PausedCallView.kt b/app/src/main/java/org/linphone/activities/call/views/PausedCallView.kt
deleted file mode 100644
index a5c9ffce4..000000000
--- a/app/src/main/java/org/linphone/activities/call/views/PausedCallView.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright (c) 2010-2020 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.linphone.activities.call.views
-
-import android.content.Context
-import android.os.SystemClock
-import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.widget.LinearLayout
-import androidx.databinding.DataBindingUtil
-import org.linphone.R
-import org.linphone.activities.call.viewmodels.CallViewModel
-import org.linphone.databinding.CallPausedBinding
-
-class PausedCallView : LinearLayout {
- private lateinit var binding: CallPausedBinding
-
- constructor(context: Context) : super(context) {
- init(context)
- }
-
- constructor(context: Context, attrs: AttributeSet) : super(
- context,
- attrs
- ) {
- init(context)
- }
-
- constructor(
- context: Context,
- attrs: AttributeSet,
- defStyleAttr: Int
- ) : super(context, attrs, defStyleAttr) {
- init(context)
- }
-
- fun init(context: Context) {
- binding = DataBindingUtil.inflate(
- LayoutInflater.from(context), R.layout.call_paused, this, true
- )
- }
-
- fun setViewModel(viewModel: CallViewModel) {
- binding.viewModel = viewModel
-
- binding.callTimer.base =
- SystemClock.elapsedRealtime() - (1000 * viewModel.call.duration) // Linphone timestamps are in seconds
- binding.callTimer.start()
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/chat_bubble/ChatBubbleActivity.kt b/app/src/main/java/org/linphone/activities/chat_bubble/ChatBubbleActivity.kt
index 08d3d8d1e..9dfa3387c 100644
--- a/app/src/main/java/org/linphone/activities/chat_bubble/ChatBubbleActivity.kt
+++ b/app/src/main/java/org/linphone/activities/chat_bubble/ChatBubbleActivity.kt
@@ -119,7 +119,7 @@ class ChatBubbleActivity : GenericActivity() {
adapter.registerAdapterDataObserver(observer)
// Disable context menu on each message
- adapter.disableContextMenu()
+ adapter.disableAdvancedContextMenuOptions()
adapter.openContentEvent.observe(
this
diff --git a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt
index 12a69892f..83ebbe442 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt
@@ -40,11 +40,11 @@ import org.linphone.activities.main.chat.data.EventData
import org.linphone.activities.main.chat.data.EventLogData
import org.linphone.activities.main.chat.data.OnContentClickedListener
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
-import org.linphone.core.ChatMessage
-import org.linphone.core.ChatRoomCapabilities
-import org.linphone.core.Content
-import org.linphone.core.EventLog
-import org.linphone.databinding.*
+import org.linphone.core.*
+import org.linphone.databinding.ChatEventListCellBinding
+import org.linphone.databinding.ChatMessageListCellBinding
+import org.linphone.databinding.ChatMessageLongPressMenuBindingImpl
+import org.linphone.databinding.ChatUnreadMessagesListHeaderBinding
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
import org.linphone.utils.HeaderAdapter
@@ -90,6 +90,10 @@ class ChatMessagesListAdapter(
MutableLiveData>()
}
+ val callConferenceEvent: MutableLiveData>> by lazy {
+ MutableLiveData>>()
+ }
+
val scrollToChatMessageEvent: MutableLiveData> by lazy {
MutableLiveData>()
}
@@ -102,9 +106,13 @@ class ChatMessagesListAdapter(
override fun onSipAddressClicked(sipUri: String) {
sipUriClickedEvent.value = Event(sipUri)
}
+
+ override fun onCallConference(address: String, subject: String?) {
+ callConferenceEvent.value = Event(Pair(address, subject))
+ }
}
- private var contextMenuDisabled: Boolean = false
+ private var advancedContextMenuOptionsDisabled: Boolean = false
private var unreadMessagesCount: Int = 0
private var firstUnreadMessagePosition: Int = -1
@@ -170,8 +178,8 @@ class ChatMessagesListAdapter(
return binding.root
}
- fun disableContextMenu() {
- contextMenuDisabled = true
+ fun disableAdvancedContextMenuOptions() {
+ advancedContextMenuOptionsDisabled = true
}
fun setUnreadMessageCount(count: Int, forceUpdate: Boolean) {
@@ -269,8 +277,6 @@ class ChatMessagesListAdapter(
executePendingBindings()
- if (contextMenuDisabled) return
-
setContextMenuClickListener {
val popupView: ChatMessageLongPressMenuBindingImpl = DataBindingUtil.inflate(
LayoutInflater.from(root.context),
@@ -292,7 +298,10 @@ class ChatMessagesListAdapter(
popupView.copyTextHidden = true
totalSize -= itemSize
}
- if (chatMessage.isOutgoing || chatMessageViewModel.contact.value != null) {
+ if (chatMessage.isOutgoing ||
+ chatMessageViewModel.contact.value != null ||
+ advancedContextMenuOptionsDisabled
+ ) {
popupView.addToContactsHidden = true
totalSize -= itemSize
}
@@ -300,6 +309,10 @@ class ChatMessagesListAdapter(
popupView.replyHidden = true
totalSize -= itemSize
}
+ if (advancedContextMenuOptionsDisabled) {
+ popupView.forwardHidden = true
+ totalSize -= itemSize
+ }
// When using WRAP_CONTENT instead of real size, fails to place the
// popup window above if not enough space is available below
diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt
index 8c81a62cf..df0e86c83 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt
@@ -27,8 +27,12 @@ import android.text.style.UnderlineSpan
import android.widget.Toast
import androidx.lifecycle.MutableLiveData
import androidx.media.AudioFocusRequestCompat
+import java.io.BufferedReader
+import java.io.FileReader
+import java.lang.StringBuilder
import java.text.SimpleDateFormat
import java.util.*
+import java.util.concurrent.TimeUnit
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
@@ -40,6 +44,7 @@ import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
import org.linphone.utils.FileUtils
import org.linphone.utils.ImageUtils
+import org.linphone.utils.TimestampUtils
class ChatMessageContentData(
private val chatMessage: ChatMessage,
@@ -56,6 +61,7 @@ class ChatMessageContentData(
val isPdf = MutableLiveData()
val isGenericFile = MutableLiveData()
val isVoiceRecording = MutableLiveData()
+ val isConferenceSchedule = MutableLiveData()
val fileName = MutableLiveData()
val filePath = MutableLiveData()
@@ -71,6 +77,15 @@ class ChatMessageContentData(
val voiceRecordPlayingPosition = MutableLiveData()
val isVoiceRecordPlaying = MutableLiveData()
+ val conferenceSubject = MutableLiveData()
+ val conferenceDescription = MutableLiveData()
+ val conferenceParticipantCount = MutableLiveData()
+ val conferenceDate = MutableLiveData()
+ val conferenceTime = MutableLiveData()
+ val conferenceDuration = MutableLiveData()
+ var conferenceAddress = MutableLiveData()
+ val showDuration = MutableLiveData()
+
val isAlone: Boolean
get() {
var count = 0
@@ -203,6 +218,13 @@ class ChatMessageContentData(
spannable.setSpan(UnderlineSpan(), 0, spannable.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
downloadLabel.value = spannable
+ isImage.value = false
+ isVideo.value = false
+ isAudio.value = false
+ isPdf.value = false
+ isVoiceRecording.value = false
+ isConferenceSchedule.value = false
+
if (content.isFile || (content.isFileTransfer && chatMessage.isOutgoing)) {
val path = if (isFileEncrypted) {
Log.i("[Content] Content is encrypted, requesting plain file path")
@@ -212,21 +234,27 @@ class ChatMessageContentData(
}
downloadable.value = content.filePath.orEmpty().isEmpty()
+ val isVoiceRecord = content.isVoiceRecording
+ isVoiceRecording.value = isVoiceRecord
+
+ val isConferenceIcs = content.isIcalendar
+ isConferenceSchedule.value = isConferenceIcs
+
if (path.isNotEmpty()) {
Log.i("[Content] Found displayable content: $path")
- val isVoiceRecord = content.isVoiceRecording
filePath.value = path
isImage.value = FileUtils.isExtensionImage(path)
isVideo.value = FileUtils.isExtensionVideo(path) && !isVoiceRecord
isAudio.value = FileUtils.isExtensionAudio(path) && !isVoiceRecord
isPdf.value = FileUtils.isExtensionPdf(path)
- isVoiceRecording.value = isVoiceRecord
if (isVoiceRecord) {
val duration = content.fileDuration // duration is in ms
voiceRecordDuration.value = duration
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration)
- Log.i("[Voice Recording] Duration is ${voiceRecordDuration.value} ($duration)")
+ Log.i("[Content] Voice recording duration is ${voiceRecordDuration.value} ($duration)")
+ } else if (isConferenceIcs) {
+ parseConferenceInvite(content)
}
if (isVideo.value == true) {
@@ -234,6 +262,9 @@ class ChatMessageContentData(
videoPreview.postValue(ImageUtils.getVideoPreview(path))
}
}
+ } else if (isConferenceIcs) {
+ Log.i("[Content] Found content with icalendar file")
+ parseConferenceInvite(content)
} else {
Log.w("[Content] Found ${if (content.isFile) "file" else "file transfer"} content with empty path...")
isImage.value = false
@@ -241,22 +272,81 @@ class ChatMessageContentData(
isAudio.value = false
isPdf.value = false
isVoiceRecording.value = false
+ isConferenceSchedule.value = false
}
- } else {
+ } else if (content.isFileTransfer) {
downloadable.value = true
isImage.value = FileUtils.isExtensionImage(fileName.value!!)
isVideo.value = FileUtils.isExtensionVideo(fileName.value!!)
isAudio.value = FileUtils.isExtensionAudio(fileName.value!!)
isPdf.value = FileUtils.isExtensionPdf(fileName.value!!)
isVoiceRecording.value = false
+ isConferenceSchedule.value = false
+ } else if (content.isIcalendar) {
+ Log.i("[Content] Found content with icalendar body")
+ isConferenceSchedule.value = true
+ parseConferenceInvite(content)
+ } else {
+ Log.w("[Content] Found content that's neither a file or a file transfer")
}
- isGenericFile.value = !isPdf.value!! && !isAudio.value!! && !isVideo.value!! && !isImage.value!! && !isVoiceRecording.value!!
+ isGenericFile.value = !isPdf.value!! && !isAudio.value!! && !isVideo.value!! && !isImage.value!! && !isVoiceRecording.value!! && !isConferenceSchedule.value!!
downloadEnabled.value = !chatMessage.isFileTransferInProgress
downloadProgressInt.value = 0
downloadProgressString.value = "0%"
}
+ private fun parseConferenceInvite(content: Content) {
+ val conferenceInfo = Factory.instance().createConferenceInfoFromIcalendarContent(content)
+ val conferenceUri = conferenceInfo?.uri?.asStringUriOnly()
+ if (conferenceInfo != null && conferenceUri != null) {
+ conferenceAddress.value = conferenceUri!!
+ Log.i("[Content] Created conference info from ICS with address ${conferenceAddress.value}")
+ conferenceSubject.value = conferenceInfo.subject
+ conferenceDescription.value = conferenceInfo.description
+
+ conferenceDate.value = TimestampUtils.dateToString(conferenceInfo.dateTime)
+ conferenceTime.value = TimestampUtils.timeToString(conferenceInfo.dateTime)
+
+ val minutes = conferenceInfo.duration
+ val hours = TimeUnit.MINUTES.toHours(minutes.toLong())
+ val remainMinutes = minutes - TimeUnit.HOURS.toMinutes(hours).toInt()
+ conferenceDuration.value = TimestampUtils.durationToString(hours.toInt(), remainMinutes)
+ showDuration.value = minutes > 0
+
+ conferenceParticipantCount.value = String.format(AppUtils.getString(R.string.conference_invite_participants_count), conferenceInfo.participants.size + 1) // +1 for organizer
+ } else if (conferenceInfo == null) {
+ if (content.filePath != null) {
+ try {
+ val br = BufferedReader(FileReader(content.filePath))
+ var line: String?
+ val textBuilder = StringBuilder()
+ while (br.readLine().also { line = it } != null) {
+ textBuilder.append(line)
+ textBuilder.append('\n')
+ }
+ br.close()
+ Log.e("[Content] Failed to create conference info from ICS file [${content.filePath}]: $textBuilder")
+ } catch (e: Exception) {
+ Log.e("[Content] Failed to read content of ICS file [${content.filePath}]: $e")
+ }
+ } else {
+ Log.e("[Content] Failed to create conference info from ICS: ${content.utf8Text}")
+ }
+ } else if (conferenceInfo.uri == null) {
+ Log.e("[Content] Failed to find the conference URI in conference info [$conferenceInfo]")
+ }
+ }
+
+ fun callConferenceAddress() {
+ val address = conferenceAddress.value
+ if (address == null) {
+ Log.e("[Content] Can't call null conference address!")
+ return
+ }
+ listener?.onCallConference(address, conferenceSubject.value)
+ }
+
/** Voice recording specifics */
fun playVoiceRecording() {
@@ -359,4 +449,6 @@ interface OnContentClickedListener {
fun onContentClicked(content: Content)
fun onSipAddressClicked(sipUri: String)
+
+ fun onCallConference(address: String, subject: String?)
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt
index 3f588d008..993a74c99 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt
@@ -177,7 +177,7 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
val contentsList = chatMessage.contents
for (index in contentsList.indices) {
val content = contentsList[index]
- if (content.isFileTransfer || content.isFile) {
+ if (content.isFileTransfer || content.isFile || content.isIcalendar) {
val data = ChatMessageContentData(chatMessage, index)
data.listener = contentListener
list.add(data)
@@ -194,6 +194,8 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
}
}
).build(spannable)
+ } else {
+ Log.e("[Chat Message Data] Unexpected content with type: ${content.type}/${content.subtype}")
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListChildData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListChildData.kt
index d4a51b937..3052871d9 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListChildData.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListChildData.kt
@@ -44,6 +44,6 @@ class DevicesListChildData(private val device: ParticipantDevice) {
}
fun onClick() {
- coreContext.startCall(device.address, true)
+ coreContext.startCall(device.address, forceZRTP = true)
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListGroupData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListGroupData.kt
index 3d1e624be..bc3141943 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListGroupData.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListGroupData.kt
@@ -68,6 +68,6 @@ class DevicesListGroupData(private val participant: Participant) : GenericContac
}
fun onClick() {
- if (device?.address != null) coreContext.startCall(device.address, true)
+ if (device?.address != null) coreContext.startCall(device.address, forceZRTP = true)
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt
index 95b08e3bf..069d90a24 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt
@@ -25,16 +25,16 @@ import android.view.View
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
-import org.linphone.LinphoneApplication
+import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.activities.main.MainActivity
-import org.linphone.activities.main.chat.adapters.ChatRoomCreationContactsAdapter
import org.linphone.activities.main.chat.viewmodels.ChatRoomCreationViewModel
import org.linphone.activities.main.fragments.SecureFragment
import org.linphone.activities.main.viewmodels.SharedMainViewModel
import org.linphone.activities.navigateToChatRoom
import org.linphone.activities.navigateToEmptyChatRoom
import org.linphone.activities.navigateToGroupInfo
+import org.linphone.contact.ContactsSelectionAdapter
import org.linphone.core.tools.Log
import org.linphone.databinding.ChatRoomCreationFragmentBinding
import org.linphone.utils.AppUtils
@@ -44,7 +44,7 @@ import org.linphone.utils.PermissionHelper
class ChatRoomCreationFragment : SecureFragment() {
private lateinit var viewModel: ChatRoomCreationViewModel
private lateinit var sharedViewModel: SharedMainViewModel
- private lateinit var adapter: ChatRoomCreationContactsAdapter
+ private lateinit var adapter: ContactsSelectionAdapter
override fun getLayoutId(): Int = R.layout.chat_room_creation_fragment
@@ -68,9 +68,9 @@ class ChatRoomCreationFragment : SecureFragment
binding.viewModel = viewModel
- adapter = ChatRoomCreationContactsAdapter(viewLifecycleOwner)
- adapter.groupChatEnabled = viewModel.createGroupChat.value == true
- adapter.updateSecurity(viewModel.isEncrypted.value == true)
+ adapter = ContactsSelectionAdapter(viewLifecycleOwner)
+ adapter.setGroupChatCapabilityRequired(viewModel.createGroupChat.value == true)
+ adapter.setLimeCapabilityRequired(viewModel.isEncrypted.value == true)
binding.contactsList.adapter = adapter
val layoutManager = LinearLayoutManager(activity)
@@ -101,7 +101,7 @@ class ChatRoomCreationFragment : SecureFragment
viewModel.isEncrypted.observe(
viewLifecycleOwner
) {
- adapter.updateSecurity(it)
+ adapter.setLimeCapabilityRequired(it)
}
viewModel.sipContactsSelected.observe(
@@ -152,7 +152,7 @@ class ChatRoomCreationFragment : SecureFragment
navigateToGroupInfo()
}
- viewModel.onErrorEvent.observe(
+ viewModel.onMessageToNotifyEvent.observe(
viewLifecycleOwner
) {
it.consume { messageResourceId ->
@@ -185,8 +185,8 @@ class ChatRoomCreationFragment : SecureFragment
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
if (granted) {
Log.i("[Chat Room Creation] READ_CONTACTS permission granted")
- LinphoneApplication.coreContext.contactsManager.onReadContactsPermissionGranted()
- LinphoneApplication.coreContext.contactsManager.fetchContactsAsync()
+ coreContext.contactsManager.onReadContactsPermissionGranted()
+ coreContext.contactsManager.fetchContactsAsync()
} else {
Log.w("[Chat Room Creation] READ_CONTACTS permission denied")
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt
index f8c2e012e..a2041beb4 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt
@@ -161,7 +161,6 @@ class DetailChatRoomFragment : MasterFragment
- val path = content.filePath.orEmpty()
+ var path = content.filePath.orEmpty()
if (!File(path).exists()) {
(requireActivity() as MainActivity).showSnackBar(R.string.chat_room_file_not_found)
} else {
+ if (path.isEmpty()) {
+ val name = content.name
+ if (name != null && name.isNotEmpty()) {
+ val file = FileUtils.getFileStoragePath(name)
+ FileUtils.writeIntoFile(content.buffer, file)
+ path = file.absolutePath
+ content.filePath = path
+ Log.i("[Chat Message] Content file path was empty, created file from buffer at $path")
+ } else if (content.isIcalendar) {
+ val name = "conference.ics"
+ val file = FileUtils.getFileStoragePath(name)
+ FileUtils.writeIntoFile(content.buffer, file)
+ path = file.absolutePath
+ content.filePath = path
+ Log.i("[Chat Message] Content file path was empty, created conference.ics from buffer at $path")
+ }
+ }
+
Log.i("[Chat Message] Opening file: $path")
sharedViewModel.contentToOpen.value = content
@@ -470,6 +487,14 @@ class DetailChatRoomFragment : MasterFragment
+ navigateToConferenceWaitingRoom(pair.first, pair.second)
+ }
+ }
+
adapter.scrollToChatMessageEvent.observe(
viewLifecycleOwner
) {
@@ -542,7 +567,7 @@ class DetailChatRoomFragment : MasterFragment() {
val chatRoom = sharedViewModel.selectedChatRoom.value
if (chatRoom == null) {
Log.e("[Devices] Chat room is null, aborting!")
- // (activity as MainActivity).showSnackBar(R.string.error)
findNavController().navigateUp()
return
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/EphemeralFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/EphemeralFragment.kt
index 3eed8654b..310ca4724 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/fragments/EphemeralFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/EphemeralFragment.kt
@@ -52,7 +52,6 @@ class EphemeralFragment : SecureFragment() {
val chatRoom = sharedViewModel.selectedChatRoom.value
if (chatRoom == null) {
Log.e("[Ephemeral] Chat room is null, aborting!")
- // (activity as MainActivity).showSnackBar(R.string.error)
findNavController().navigateUp()
return
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt
index 4baa685a3..9e5ea8c16 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt
@@ -175,7 +175,7 @@ class GroupInfoFragment : SecureFragment() {
dialog.show()
}
- viewModel.onErrorEvent.observe(
+ viewModel.onMessageToNotifyEvent.observe(
viewLifecycleOwner
) {
it.consume { messageResourceId ->
diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt
index e50e5d0a1..7b422a491 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt
@@ -56,7 +56,6 @@ class ImdnFragment : SecureFragment() {
val chatRoom = sharedViewModel.selectedChatRoom.value
if (chatRoom == null) {
Log.e("[IMDN] Chat room is null, aborting!")
- // (activity as MainActivity).showSnackBar(R.string.error)
findNavController().navigateUp()
return
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt
index eff7aedc1..d2626aa41 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt
@@ -312,8 +312,6 @@ class MasterChatRoomsFragment : MasterFragment
diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt
index bb1cc356e..24ced613a 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt
@@ -23,43 +23,26 @@ import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
-import org.linphone.activities.main.viewmodels.ErrorReportingViewModel
-import org.linphone.contact.ContactsUpdatedListenerStub
+import org.linphone.contact.ContactsSelectionViewModel
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
-class ChatRoomCreationViewModel : ErrorReportingViewModel() {
+class ChatRoomCreationViewModel : ContactsSelectionViewModel() {
val chatRoomCreatedEvent: MutableLiveData> by lazy {
MutableLiveData>()
}
val createGroupChat = MutableLiveData()
- val sipContactsSelected = MutableLiveData()
-
val isEncrypted = MutableLiveData()
- val contactsList = MutableLiveData>()
-
val waitForChatRoomCreation = MutableLiveData()
- val selectedAddresses = MutableLiveData>()
-
- val filter = MutableLiveData()
- private var previousFilter = ""
-
val limeAvailable: Boolean = LinphoneUtils.isLimeAvailable()
- private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() {
- override fun onContactsUpdated() {
- Log.i("[Chat Room Creation] Contacts have changed")
- updateContactsList()
- }
- }
-
private val listener = object : ChatRoomListenerStub() {
override fun onStateChanged(room: ChatRoom, state: ChatRoom.State) {
if (state == ChatRoom.State.Created) {
@@ -69,25 +52,18 @@ class ChatRoomCreationViewModel : ErrorReportingViewModel() {
} else if (state == ChatRoom.State.CreationFailed) {
Log.e("[Chat Room Creation] Group chat room creation has failed !")
waitForChatRoomCreation.value = false
- onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack)
+ onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
}
}
}
init {
createGroupChat.value = false
- sipContactsSelected.value = coreContext.contactsManager.shouldDisplaySipContactsList()
isEncrypted.value = false
-
- selectedAddresses.value = arrayListOf()
-
- coreContext.contactsManager.addListener(contactsUpdatedListener)
waitForChatRoomCreation.value = false
}
override fun onCleared() {
- coreContext.contactsManager.removeListener(contactsUpdatedListener)
-
super.onCleared()
}
@@ -95,55 +71,6 @@ class ChatRoomCreationViewModel : ErrorReportingViewModel() {
isEncrypted.value = encrypted
}
- fun applyFilter() {
- val filterValue = filter.value.orEmpty()
- if (previousFilter == filterValue) return
-
- if (previousFilter.isNotEmpty() && previousFilter.length > filterValue.length) {
- coreContext.contactsManager.magicSearch.resetSearchCache()
- }
- previousFilter = filterValue
-
- updateContactsList()
- }
-
- fun updateContactsList() {
- val domain = if (sipContactsSelected.value == true) coreContext.core.defaultAccount?.params?.domain ?: "" else ""
- val results = coreContext.contactsManager.magicSearch.getContactListFromFilter(filter.value.orEmpty(), domain)
-
- val list = arrayListOf()
- for (result in results) {
- list.add(result)
- }
- contactsList.value = list
- }
-
- fun toggleSelectionForSearchResult(searchResult: SearchResult) {
- val address = searchResult.address
- if (address != null) {
- toggleSelectionForAddress(address)
- }
- }
-
- fun toggleSelectionForAddress(address: Address) {
- val list = arrayListOf()
- list.addAll(selectedAddresses.value.orEmpty())
-
- val found = list.find {
- it.weakEqual(address)
- }
-
- if (found != null) {
- list.remove(found)
- } else {
- val contact = coreContext.contactsManager.findContactByAddress(address)
- if (contact != null) address.displayName = contact.fullName
- list.add(address)
- }
-
- selectedAddresses.value = list
- }
-
fun createOneToOneChat(searchResult: SearchResult) {
waitForChatRoomCreation.value = true
val defaultAccount = coreContext.core.defaultAccount
@@ -152,7 +79,7 @@ class ChatRoomCreationViewModel : ErrorReportingViewModel() {
val address = searchResult.address ?: coreContext.core.interpretUrl(searchResult.phoneNumber ?: "")
if (address == null) {
Log.e("[Chat Room Creation] Can't get a valid address from search result $searchResult")
- onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack)
+ onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
waitForChatRoomCreation.value = false
return
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt
index b84b82090..3a06cc41f 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt
@@ -20,6 +20,9 @@
package org.linphone.activities.main.chat.viewmodels
import android.animation.ValueAnimator
+import android.graphics.Typeface
+import android.text.SpannableStringBuilder
+import android.text.style.StyleSpan
import android.view.animation.LinearInterpolator
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
@@ -60,7 +63,7 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
val lastUpdate = MutableLiveData()
- val lastMessageText = MutableLiveData()
+ val lastMessageText = MutableLiveData()
val callInProgress = MutableLiveData()
@@ -113,7 +116,7 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
override fun onContactsUpdated() {
Log.i("[Chat Room] Contacts have changed")
contactLookup()
- updateLastMessageToDisplay()
+ formatLastMessage(chatRoom.lastMessageInHistory)
}
}
@@ -150,11 +153,11 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
override fun onChatMessageReceived(chatRoom: ChatRoom, eventLog: EventLog) {
unreadMessagesCount.value = chatRoom.unreadMessagesCount
- lastMessageText.value = formatLastMessage(eventLog.chatMessage)
+ formatLastMessage(eventLog.chatMessage)
}
override fun onChatMessageSent(chatRoom: ChatRoom, eventLog: EventLog) {
- lastMessageText.value = formatLastMessage(eventLog.chatMessage)
+ formatLastMessage(eventLog.chatMessage)
}
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
@@ -199,7 +202,7 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) {
Log.i("[Chat Room] Ephemeral message deleted, updated last message displayed")
- updateLastMessageToDisplay()
+ formatLastMessage(chatRoom.lastMessageInHistory)
}
override fun onEphemeralEvent(chatRoom: ChatRoom, eventLog: EventLog) {
@@ -216,6 +219,7 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
chatRoom.addListener(chatRoomListener)
coreContext.contactsManager.addListener(contactsUpdatedListener)
+ formatLastMessage(chatRoom.lastMessageInHistory)
unreadMessagesCount.value = chatRoom.unreadMessagesCount
lastUpdate.value = TimestampUtils.toString(chatRoom.lastUpdateTime, true)
@@ -226,7 +230,7 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
contactLookup()
updateParticipants()
- updateLastMessageToDisplay()
+ formatLastMessage(chatRoom.lastMessageInHistory)
callInProgress.value = chatRoom.core.callsNb > 0
updateRemotesComposing()
@@ -277,22 +281,36 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
}
fun updateLastMessageToDisplay() {
- lastMessageText.value = formatLastMessage(chatRoom.lastMessageInHistory)
+ formatLastMessage(chatRoom.lastMessageInHistory)
}
- private fun formatLastMessage(msg: ChatMessage?): String {
- if (msg == null) return ""
+ private fun formatLastMessage(msg: ChatMessage?) {
+ val builder = SpannableStringBuilder()
+ if (msg == null) {
+ lastMessageText.value = builder
+ return
+ }
val sender: String =
coreContext.contactsManager.findContactByAddress(msg.fromAddress)?.fullName
?: LinphoneUtils.getDisplayName(msg.fromAddress)
- var body = ""
+ builder.append(sender)
+ builder.append(": ")
+
for (content in msg.contents) {
- if (content.isFile || content.isFileTransfer) body += content.name + " "
- else if (content.isText) body += content.utf8Text + " "
+ if (content.isIcalendar) {
+ val body = AppUtils.getString(R.string.conference_invitation)
+ builder.append(body)
+ builder.setSpan(StyleSpan(Typeface.ITALIC), builder.length - body.length, builder.length, 0)
+ } else if (content.isFile || content.isFileTransfer) {
+ builder.append(content.name + " ")
+ } else if (content.isText) {
+ builder.append(content.utf8Text + " ")
+ }
}
- return "$sender: $body"
+ builder.trim()
+ lastMessageText.value = builder
}
private fun searchMatchingContact() {
diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt
index 1e9982c5f..7982d9fcf 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt
@@ -22,7 +22,7 @@ package org.linphone.activities.main.chat.viewmodels
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
-import org.linphone.activities.main.viewmodels.ErrorReportingViewModel
+import org.linphone.activities.main.viewmodels.MessageNotifierViewModel
import org.linphone.compatibility.Compatibility
import org.linphone.contact.ContactsUpdatedListenerStub
import org.linphone.core.*
@@ -30,7 +30,7 @@ import org.linphone.core.tools.Log
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
-class ChatRoomsListViewModel : ErrorReportingViewModel() {
+class ChatRoomsListViewModel : MessageNotifierViewModel() {
val chatRooms = MutableLiveData>()
val contactsUpdatedEvent: MutableLiveData> by lazy {
@@ -60,7 +60,7 @@ class ChatRoomsListViewModel : ErrorReportingViewModel() {
}
} else if (state == ChatRoom.State.TerminationFailed) {
Log.e("[Chat Rooms] Group chat room removal for address ${chatRoom.peerAddress.asStringUriOnly()} has failed !")
- onErrorEvent.value = Event(R.string.chat_room_removal_failed_snack)
+ onMessageToNotifyEvent.value = Event(R.string.chat_room_removal_failed_snack)
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt
index 7bfd94e48..807698f62 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt
@@ -27,7 +27,7 @@ import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.activities.main.chat.GroupChatRoomMember
import org.linphone.activities.main.chat.data.GroupInfoParticipantData
-import org.linphone.activities.main.viewmodels.ErrorReportingViewModel
+import org.linphone.activities.main.viewmodels.MessageNotifierViewModel
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.Event
@@ -41,7 +41,7 @@ class GroupInfoViewModelFactory(private val chatRoom: ChatRoom?) :
}
}
-class GroupInfoViewModel(val chatRoom: ChatRoom?) : ErrorReportingViewModel() {
+class GroupInfoViewModel(val chatRoom: ChatRoom?) : MessageNotifierViewModel() {
val createdChatRoomEvent = MutableLiveData>()
val updatedChatRoomEvent = MutableLiveData>()
@@ -69,7 +69,7 @@ class GroupInfoViewModel(val chatRoom: ChatRoom?) : ErrorReportingViewModel() {
} else if (state == ChatRoom.State.CreationFailed) {
Log.e("[Chat Room Group Info] Group chat room creation has failed !")
waitForChatRoomCreation.value = false
- onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack)
+ onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
}
}
@@ -142,7 +142,7 @@ class GroupInfoViewModel(val chatRoom: ChatRoom?) : ErrorReportingViewModel() {
if (chatRoom == null) {
Log.e("[Chat Room Group Info] Couldn't create chat room!")
waitForChatRoomCreation.value = false
- onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack)
+ onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/conference/adapters/ScheduledConferencesAdapter.kt b/app/src/main/java/org/linphone/activities/main/conference/adapters/ScheduledConferencesAdapter.kt
new file mode 100644
index 000000000..a3ce4ac64
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/conference/adapters/ScheduledConferencesAdapter.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.activities.main.conference.adapters
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.MutableLiveData
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import org.linphone.R
+import org.linphone.activities.main.conference.data.ScheduledConferenceData
+import org.linphone.core.Address
+import org.linphone.databinding.ConferenceScheduleCellBinding
+import org.linphone.databinding.ConferenceScheduleListHeaderBinding
+import org.linphone.utils.Event
+import org.linphone.utils.HeaderAdapter
+import org.linphone.utils.TimestampUtils
+
+class ScheduledConferencesAdapter(
+ private val viewLifecycleOwner: LifecycleOwner
+) : ListAdapter(ConferenceInfoDiffCallback()),
+ HeaderAdapter {
+ val copyAddressToClipboardEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val joinConferenceEvent: MutableLiveData>> by lazy {
+ MutableLiveData>>()
+ }
+
+ val deleteConferenceInfoEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScheduledConferencesAdapter.ViewHolder {
+ val binding: ConferenceScheduleCellBinding = DataBindingUtil.inflate(
+ LayoutInflater.from(parent.context),
+ R.layout.conference_schedule_cell, parent, false
+ )
+ return ViewHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ (holder as ScheduledConferencesAdapter.ViewHolder).bind(getItem(position))
+ }
+
+ override fun displayHeaderForPosition(position: Int): Boolean {
+ if (position >= itemCount) return false
+ val conferenceInfo = getItem(position)
+ val previousPosition = position - 1
+ return if (previousPosition >= 0) {
+ val previousItem = getItem(previousPosition)
+ !TimestampUtils.isSameDay(previousItem.conferenceInfo.dateTime, conferenceInfo.conferenceInfo.dateTime)
+ } else true
+ }
+
+ override fun getHeaderViewForPosition(context: Context, position: Int): View {
+ val data = getItem(position)
+ val binding: ConferenceScheduleListHeaderBinding = DataBindingUtil.inflate(
+ LayoutInflater.from(context),
+ R.layout.conference_schedule_list_header, null, false
+ )
+ binding.title = formatDate(context, data.conferenceInfo.dateTime)
+ binding.executePendingBindings()
+ return binding.root
+ }
+
+ private fun formatDate(context: Context, date: Long): String {
+ if (TimestampUtils.isToday(date)) {
+ return context.getString(R.string.today)
+ }
+ return TimestampUtils.toString(date, onlyDate = true, shortDate = false, hideYear = false)
+ }
+
+ inner class ViewHolder(
+ val binding: ConferenceScheduleCellBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+ fun bind(conferenceData: ScheduledConferenceData) {
+ with(binding) {
+ data = conferenceData
+
+ lifecycleOwner = viewLifecycleOwner
+
+ setCopyAddressClickListener {
+ val address = conferenceData.conferenceInfo.uri
+ if (address != null) {
+ copyAddressToClipboardEvent.value = Event(address)
+ }
+ }
+
+ setJoinConferenceClickListener {
+ val address = conferenceData.conferenceInfo.uri
+ if (address != null) {
+ joinConferenceEvent.value = Event(Pair(address.asStringUriOnly(), conferenceData.conferenceInfo.subject))
+ }
+ }
+
+ setDeleteConferenceClickListener {
+ deleteConferenceInfoEvent.value = Event(conferenceData)
+ }
+
+ executePendingBindings()
+ }
+ }
+ }
+}
+
+private class ConferenceInfoDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: ScheduledConferenceData,
+ newItem: ScheduledConferenceData
+ ): Boolean {
+ return oldItem.conferenceInfo == newItem.conferenceInfo
+ }
+
+ override fun areContentsTheSame(
+ oldItem: ScheduledConferenceData,
+ newItem: ScheduledConferenceData
+ ): Boolean {
+ return false
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/conference/data/ConferenceSchedulingParticipantData.kt b/app/src/main/java/org/linphone/activities/main/conference/data/ConferenceSchedulingParticipantData.kt
new file mode 100644
index 000000000..71d9f0f87
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/conference/data/ConferenceSchedulingParticipantData.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.activities.main.conference.data
+
+import org.linphone.contact.GenericContactData
+import org.linphone.core.Address
+import org.linphone.utils.LinphoneUtils
+
+class ConferenceSchedulingParticipantData(
+ private val sipAddress: Address,
+ val showLimeBadge: Boolean
+) :
+ GenericContactData(sipAddress) {
+ val sipUri: String get() = LinphoneUtils.getDisplayableAddress(sipAddress)
+}
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/SharedCallViewModel.kt b/app/src/main/java/org/linphone/activities/main/conference/data/Duration.kt
similarity index 64%
rename from app/src/main/java/org/linphone/activities/call/viewmodels/SharedCallViewModel.kt
rename to app/src/main/java/org/linphone/activities/main/conference/data/Duration.kt
index a10b96fe3..61a6ee696 100644
--- a/app/src/main/java/org/linphone/activities/call/viewmodels/SharedCallViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/conference/data/Duration.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
@@ -17,14 +17,14 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.linphone.activities.call.viewmodels
+package org.linphone.activities.main.conference.data
-import androidx.lifecycle.MutableLiveData
-import androidx.lifecycle.ViewModel
-import org.linphone.utils.Event
+class Duration(val value: Int, val display: String) : Comparable {
+ override fun toString(): String {
+ return display
+ }
-class SharedCallViewModel : ViewModel() {
- val toggleDrawerEvent = MutableLiveData>()
-
- val resetHiddenInterfaceTimerInVideoCallEvent = MutableLiveData>()
+ override fun compareTo(other: Duration): Int {
+ return value.compareTo(other.value)
+ }
}
diff --git a/app/src/main/java/org/linphone/activities/main/conference/data/ScheduledConferenceData.kt b/app/src/main/java/org/linphone/activities/main/conference/data/ScheduledConferenceData.kt
new file mode 100644
index 000000000..feff7598f
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/conference/data/ScheduledConferenceData.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.activities.main.conference.data
+
+import androidx.lifecycle.MutableLiveData
+import java.util.concurrent.TimeUnit
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.core.ConferenceInfo
+import org.linphone.core.tools.Log
+import org.linphone.utils.LinphoneUtils
+import org.linphone.utils.TimestampUtils
+
+class ScheduledConferenceData(val conferenceInfo: ConferenceInfo) {
+ val expanded = MutableLiveData()
+
+ val address = MutableLiveData()
+ val subject = MutableLiveData()
+ val description = MutableLiveData()
+ val time = MutableLiveData()
+ val date = MutableLiveData()
+ val duration = MutableLiveData()
+ val organizer = MutableLiveData()
+ val participantsShort = MutableLiveData()
+ val participantsExpanded = MutableLiveData()
+ val showDuration = MutableLiveData()
+
+ init {
+ expanded.value = false
+
+ address.value = conferenceInfo.uri?.asStringUriOnly()
+ subject.value = conferenceInfo.subject
+ description.value = conferenceInfo.description
+
+ time.value = TimestampUtils.timeToString(conferenceInfo.dateTime)
+ date.value = TimestampUtils.toString(conferenceInfo.dateTime, onlyDate = true, shortDate = false, hideYear = false)
+
+ val minutes = conferenceInfo.duration
+ val hours = TimeUnit.MINUTES.toHours(minutes.toLong())
+ val remainMinutes = minutes - TimeUnit.HOURS.toMinutes(hours).toInt()
+ duration.value = TimestampUtils.durationToString(hours.toInt(), remainMinutes)
+ showDuration.value = minutes > 0
+
+ val organizerAddress = conferenceInfo.organizer
+ if (organizerAddress != null) {
+ val contact = coreContext.contactsManager.findContactByAddress(organizerAddress)
+ organizer.value = if (contact != null)
+ contact.fullName
+ else
+ LinphoneUtils.getDisplayName(conferenceInfo.organizer)
+ } else {
+ Log.e("[Scheduled Conference] No organizer SIP URI found for: ${conferenceInfo.uri?.asStringUriOnly()}")
+ }
+
+ computeParticipantsLists()
+ }
+
+ fun destroy() {}
+
+ fun delete() {
+ Log.w("[Scheduled Conference] Deleting conference info with URI: ${conferenceInfo.uri?.asStringUriOnly()}")
+ coreContext.core.deleteConferenceInformation(conferenceInfo)
+ }
+
+ fun toggleExpand() {
+ expanded.value = expanded.value == false
+ }
+
+ private fun computeParticipantsLists() {
+ var participantsListShort = ""
+ var participantsListExpanded = ""
+
+ for (participant in conferenceInfo.participants) {
+ val contact = coreContext.contactsManager.findContactByAddress(participant)
+ val name = if (contact != null) contact.fullName else LinphoneUtils.getDisplayName(participant)
+ val address = participant.asStringUriOnly()
+ participantsListShort += "$name, "
+ participantsListExpanded += "$name ($address)\n"
+ }
+ participantsListShort = participantsListShort.dropLast(2)
+ participantsListExpanded = participantsListExpanded.dropLast(1)
+
+ participantsShort.value = participantsListShort
+ participantsExpanded.value = participantsListExpanded
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/conference/data/TimeZoneData.kt b/app/src/main/java/org/linphone/activities/main/conference/data/TimeZoneData.kt
new file mode 100644
index 000000000..de9007e53
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/conference/data/TimeZoneData.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.activities.main.conference.data
+
+import java.util.*
+import java.util.concurrent.TimeUnit
+import kotlin.math.abs
+
+class TimeZoneData(timeZone: TimeZone) : Comparable {
+ val id: String = timeZone.id
+ private val hours: Long
+ private val minutes: Long
+ private val gmt: String
+
+ init {
+ hours = TimeUnit.MILLISECONDS.toHours(timeZone.rawOffset.toLong())
+ minutes = abs(
+ TimeUnit.MILLISECONDS.toMinutes(timeZone.rawOffset.toLong()) -
+ TimeUnit.HOURS.toMinutes(hours)
+ )
+
+ gmt = if (hours > 0) {
+ String.format("%s - GMT+%d:%02d", timeZone.id, hours, minutes)
+ } else {
+ String.format("%s - GMT%d:%02d", timeZone.id, hours, minutes)
+ }
+ }
+
+ override fun toString(): String {
+ return gmt
+ }
+
+ override fun compareTo(other: TimeZoneData): Int {
+ if (hours == other.hours) {
+ if (minutes == other.minutes) {
+ return id.compareTo(other.id)
+ }
+ return minutes.compareTo(other.minutes)
+ }
+ return hours.compareTo(other.hours)
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingFragment.kt b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingFragment.kt
new file mode 100644
index 000000000..23e1fb610
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingFragment.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.activities.main.conference.fragments
+
+import android.os.Bundle
+import android.text.format.DateFormat.is24HourFormat
+import android.view.View
+import androidx.navigation.navGraphViewModels
+import com.google.android.material.datepicker.CalendarConstraints
+import com.google.android.material.datepicker.DateValidatorPointForward
+import com.google.android.material.datepicker.MaterialDatePicker
+import com.google.android.material.timepicker.MaterialTimePicker
+import com.google.android.material.timepicker.TimeFormat
+import org.linphone.R
+import org.linphone.activities.GenericFragment
+import org.linphone.activities.main.conference.viewmodels.ConferenceSchedulingViewModel
+import org.linphone.activities.navigateToParticipantsList
+import org.linphone.databinding.ConferenceSchedulingFragmentBinding
+
+class ConferenceSchedulingFragment : GenericFragment() {
+ private val viewModel: ConferenceSchedulingViewModel by navGraphViewModels(R.id.conference_scheduling_nav_graph)
+
+ override fun getLayoutId(): Int = R.layout.conference_scheduling_fragment
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.lifecycleOwner = viewLifecycleOwner
+
+ binding.viewModel = viewModel
+
+ binding.setBackClickListener {
+ goBack()
+ }
+
+ binding.setNextClickListener {
+ navigateToParticipantsList()
+ }
+
+ binding.setDatePickerClickListener {
+ val constraintsBuilder =
+ CalendarConstraints.Builder()
+ .setValidator(DateValidatorPointForward.now())
+ val picker =
+ MaterialDatePicker.Builder.datePicker()
+ .setCalendarConstraints(constraintsBuilder.build())
+ .setTitleText(R.string.conference_schedule_date)
+ .setSelection(viewModel.dateTimestamp)
+ .build()
+ picker.addOnPositiveButtonClickListener {
+ val selection = picker.selection
+ if (selection != null) {
+ viewModel.setDate(selection)
+ }
+ }
+ picker.show(requireFragmentManager(), "Date picker")
+ }
+
+ binding.setTimePickerClickListener {
+ val isSystem24Hour = is24HourFormat(requireContext())
+ val clockFormat = if (isSystem24Hour) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H
+ val picker =
+ MaterialTimePicker.Builder()
+ .setTimeFormat(clockFormat)
+ .setTitleText(R.string.conference_schedule_time)
+ .setHour(viewModel.hour)
+ .setMinute(viewModel.minutes)
+ .build()
+ picker.addOnPositiveButtonClickListener {
+ viewModel.setTime(picker.hour, picker.minute)
+ }
+ picker.show(requireFragmentManager(), "Time picker")
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingParticipantsListFragment.kt b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingParticipantsListFragment.kt
new file mode 100644
index 000000000..6f565c116
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingParticipantsListFragment.kt
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.activities.main.conference.fragments
+
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.view.View
+import androidx.navigation.navGraphViewModels
+import androidx.recyclerview.widget.LinearLayoutManager
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.activities.GenericFragment
+import org.linphone.activities.main.conference.viewmodels.ConferenceSchedulingViewModel
+import org.linphone.activities.navigateToSummary
+import org.linphone.contact.ContactsSelectionAdapter
+import org.linphone.core.tools.Log
+import org.linphone.databinding.ConferenceSchedulingParticipantsListFragmentBinding
+import org.linphone.utils.AppUtils
+import org.linphone.utils.PermissionHelper
+
+class ConferenceSchedulingParticipantsListFragment : GenericFragment() {
+ private val viewModel: ConferenceSchedulingViewModel by navGraphViewModels(R.id.conference_scheduling_nav_graph)
+ private lateinit var adapter: ContactsSelectionAdapter
+
+ override fun getLayoutId(): Int = R.layout.conference_scheduling_participants_list_fragment
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.lifecycleOwner = viewLifecycleOwner
+
+ binding.viewModel = viewModel
+
+ adapter = ContactsSelectionAdapter(viewLifecycleOwner)
+ adapter.setLimeCapabilityRequired(viewModel.isEncrypted.value == true)
+ binding.contactsList.adapter = adapter
+
+ val layoutManager = LinearLayoutManager(activity)
+ binding.contactsList.layoutManager = layoutManager
+
+ // Divider between items
+ binding.contactsList.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
+
+ binding.setBackClickListener {
+ goBack()
+ }
+
+ binding.setNextClickListener {
+ navigateToSummary()
+ }
+
+ viewModel.contactsList.observe(
+ viewLifecycleOwner
+ ) {
+ adapter.submitList(it)
+ }
+ viewModel.sipContactsSelected.observe(
+ viewLifecycleOwner
+ ) {
+ viewModel.updateContactsList()
+ }
+
+ viewModel.selectedAddresses.observe(
+ viewLifecycleOwner
+ ) {
+ adapter.updateSelectedAddresses(it)
+ }
+ viewModel.filter.observe(
+ viewLifecycleOwner
+ ) {
+ viewModel.applyFilter()
+ }
+
+ adapter.selectedContact.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume { searchResult ->
+ viewModel.toggleSelectionForSearchResult(searchResult)
+ }
+ }
+
+ if (!PermissionHelper.get().hasReadContactsPermission()) {
+ Log.i("[Conference Creation] Asking for READ_CONTACTS permission")
+ requestPermissions(arrayOf(android.Manifest.permission.READ_CONTACTS), 0)
+ }
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ if (requestCode == 0) {
+ val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
+ if (granted) {
+ Log.i("[Conference Creation] READ_CONTACTS permission granted")
+ coreContext.contactsManager.onReadContactsPermissionGranted()
+ coreContext.contactsManager.fetchContactsAsync()
+ } else {
+ Log.w("[Conference Creation] READ_CONTACTS permission denied")
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingSummaryFragment.kt b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingSummaryFragment.kt
new file mode 100644
index 000000000..4d253e0f6
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingSummaryFragment.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.activities.main.conference.fragments
+
+import android.os.Bundle
+import android.view.View
+import androidx.navigation.navGraphViewModels
+import org.linphone.R
+import org.linphone.activities.GenericFragment
+import org.linphone.activities.goToScheduledConferences
+import org.linphone.activities.main.MainActivity
+import org.linphone.activities.main.conference.viewmodels.ConferenceSchedulingViewModel
+import org.linphone.activities.navigateToConferenceWaitingRoom
+import org.linphone.databinding.ConferenceSchedulingSummaryFragmentBinding
+
+class ConferenceSchedulingSummaryFragment : GenericFragment() {
+ private val viewModel: ConferenceSchedulingViewModel by navGraphViewModels(R.id.conference_scheduling_nav_graph)
+
+ override fun getLayoutId(): Int = R.layout.conference_scheduling_summary_fragment
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.lifecycleOwner = viewLifecycleOwner
+
+ binding.viewModel = viewModel
+
+ binding.setBackClickListener {
+ goBack()
+ }
+
+ binding.setCreateConferenceClickListener {
+ viewModel.createConference()
+ }
+
+ viewModel.conferenceCreationCompletedEvent.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume { pair ->
+ if (viewModel.scheduleForLater.value == true) {
+ (requireActivity() as MainActivity).showSnackBar(R.string.conference_schedule_info_created)
+ goToScheduledConferences()
+ } else {
+ navigateToConferenceWaitingRoom(pair.first, pair.second)
+ }
+ }
+ }
+
+ viewModel.onMessageToNotifyEvent.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume { messageId ->
+ (activity as MainActivity).showSnackBar(messageId)
+ }
+ }
+
+ viewModel.computeParticipantsData()
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceWaitingRoomFragment.kt b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceWaitingRoomFragment.kt
new file mode 100644
index 000000000..869b4e95f
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceWaitingRoomFragment.kt
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.activities.main.conference.fragments
+
+import android.Manifest
+import android.annotation.TargetApi
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.view.View
+import androidx.lifecycle.ViewModelProvider
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.activities.GenericFragment
+import org.linphone.activities.main.conference.viewmodels.ConferenceWaitingRoomViewModel
+import org.linphone.activities.navigateToDialer
+import org.linphone.core.tools.Log
+import org.linphone.databinding.ConferenceWaitingRoomFragmentBinding
+import org.linphone.mediastream.Version
+import org.linphone.utils.PermissionHelper
+
+class ConferenceWaitingRoomFragment : GenericFragment() {
+ private lateinit var viewModel: ConferenceWaitingRoomViewModel
+
+ override fun getLayoutId(): Int = R.layout.conference_waiting_room_fragment
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.lifecycleOwner = viewLifecycleOwner
+
+ viewModel = ViewModelProvider(
+ this
+ )[ConferenceWaitingRoomViewModel::class.java]
+ binding.viewModel = viewModel
+
+ val conferenceSubject = arguments?.getString("Subject")
+ viewModel.subject.value = conferenceSubject
+
+ viewModel.cancelConferenceJoiningEvent.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ if (viewModel.joinInProgress.value == true) {
+ val conferenceUri = arguments?.getString("Address")
+ val callToCancel = coreContext.core.calls.find { call ->
+ call.remoteAddress.asStringUriOnly() == conferenceUri
+ }
+ if (callToCancel != null) {
+ Log.i("[Conference Waiting Room] Call to conference server with URI [$conferenceUri] was started, terminate it")
+ callToCancel.terminate()
+ } else {
+ Log.w("[Conference Waiting Room] Call to conference server with URI [$conferenceUri] wasn't found!")
+ }
+ }
+ navigateToDialer()
+ }
+ }
+
+ viewModel.joinConferenceEvent.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume { callParams ->
+ val conferenceUri = arguments?.getString("Address")
+ if (conferenceUri != null) {
+ val conferenceAddress = coreContext.core.interpretUrl(conferenceUri)
+ if (conferenceAddress != null) {
+ Log.i("[Conference Waiting Room] Calling conference SIP URI: ${conferenceAddress.asStringUriOnly()}")
+ coreContext.startCall(conferenceAddress, callParams)
+ } else {
+ Log.e("[Conference Waiting Room] Failed to parse conference SIP URI: $conferenceUri")
+ }
+ } else {
+ Log.e("[Conference Waiting Room] Failed to find conference SIP URI in arguments")
+ }
+ }
+ }
+
+ viewModel.askPermissionEvent.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume { permission ->
+ Log.i("[Conference Waiting Room] Asking for $permission permission")
+ requestPermissions(arrayOf(permission), 0)
+ }
+ }
+
+ viewModel.leaveWaitingRoomEvent.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ goBack()
+ }
+ }
+
+ if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
+ checkPermissions()
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface
+ }
+
+ override fun onPause() {
+ coreContext.core.nativePreviewWindowId = null
+
+ super.onPause()
+ }
+
+ @TargetApi(Version.API23_MARSHMALLOW_60)
+ private fun checkPermissions() {
+ val permissionsRequiredList = arrayListOf()
+ if (!PermissionHelper.get().hasRecordAudioPermission()) {
+ Log.i("[Conference Waiting Room] Asking for RECORD_AUDIO permission")
+ permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO)
+ }
+ if (!PermissionHelper.get().hasCameraPermission()) {
+ Log.i("[Conference Waiting Room] Asking for CAMERA permission")
+ permissionsRequiredList.add(Manifest.permission.CAMERA)
+ }
+ if (permissionsRequiredList.isNotEmpty()) {
+ val permissionsRequired = arrayOfNulls(permissionsRequiredList.size)
+ permissionsRequiredList.toArray(permissionsRequired)
+ requestPermissions(permissionsRequired, 0)
+ }
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ if (requestCode == 0) {
+ for (i in permissions.indices) {
+ when (permissions[i]) {
+ Manifest.permission.RECORD_AUDIO -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
+ Log.i("[Conference Waiting Room] RECORD_AUDIO permission has been granted")
+ viewModel.enableMic()
+ }
+ Manifest.permission.CAMERA -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
+ Log.i("[Conference Waiting Room] CAMERA permission has been granted")
+ coreContext.core.reloadVideoDevices()
+ viewModel.enableVideo()
+ }
+ }
+ }
+ }
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/conference/fragments/ScheduledConferencesFragment.kt b/app/src/main/java/org/linphone/activities/main/conference/fragments/ScheduledConferencesFragment.kt
new file mode 100644
index 000000000..1f74142c7
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/conference/fragments/ScheduledConferencesFragment.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.activities.main.conference.fragments
+
+import android.app.Dialog
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.LinearLayoutManager
+import org.linphone.R
+import org.linphone.activities.GenericFragment
+import org.linphone.activities.main.MainActivity
+import org.linphone.activities.main.conference.adapters.ScheduledConferencesAdapter
+import org.linphone.activities.main.conference.viewmodels.ScheduledConferencesViewModel
+import org.linphone.activities.main.viewmodels.DialogViewModel
+import org.linphone.activities.navigateToConferenceScheduling
+import org.linphone.activities.navigateToConferenceWaitingRoom
+import org.linphone.databinding.ConferencesScheduledFragmentBinding
+import org.linphone.utils.AppUtils
+import org.linphone.utils.DialogUtils
+import org.linphone.utils.RecyclerViewHeaderDecoration
+
+class ScheduledConferencesFragment : GenericFragment() {
+ private lateinit var viewModel: ScheduledConferencesViewModel
+ private lateinit var adapter: ScheduledConferencesAdapter
+
+ override fun getLayoutId(): Int = R.layout.conferences_scheduled_fragment
+
+ private var deleteConferenceInfoDialog: Dialog? = null
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.lifecycleOwner = viewLifecycleOwner
+
+ viewModel = ViewModelProvider(
+ this
+ )[ScheduledConferencesViewModel::class.java]
+ binding.viewModel = viewModel
+
+ adapter = ScheduledConferencesAdapter(
+ viewLifecycleOwner
+ )
+ binding.conferenceInfoList.adapter = adapter
+
+ val layoutManager = LinearLayoutManager(activity)
+ binding.conferenceInfoList.layoutManager = layoutManager
+
+ // Displays date header
+ val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter)
+ binding.conferenceInfoList.addItemDecoration(headerItemDecoration)
+
+ viewModel.conferences.observe(
+ viewLifecycleOwner
+ ) {
+ adapter.submitList(it)
+ }
+
+ adapter.copyAddressToClipboardEvent.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume { address ->
+ val clipboard =
+ requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText("Conference address", address.asStringUriOnly())
+ clipboard.setPrimaryClip(clip)
+
+ (activity as MainActivity).showSnackBar(R.string.conference_schedule_address_copied_to_clipboard)
+ }
+ }
+
+ adapter.joinConferenceEvent.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume { pair ->
+ navigateToConferenceWaitingRoom(pair.first, pair.second)
+ }
+ }
+
+ adapter.deleteConferenceInfoEvent.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume { data ->
+ val dialogViewModel =
+ DialogViewModel(AppUtils.getString(R.string.conference_info_confirm_removal))
+ deleteConferenceInfoDialog =
+ DialogUtils.getVoipDialog(requireContext(), dialogViewModel)
+
+ dialogViewModel.showCancelButton(
+ {
+ deleteConferenceInfoDialog?.dismiss()
+ },
+ getString(R.string.dialog_cancel)
+ )
+
+ dialogViewModel.showDeleteButton(
+ {
+ viewModel.deleteConferenceInfo(data)
+ deleteConferenceInfoDialog?.dismiss()
+ (requireActivity() as MainActivity).showSnackBar(R.string.conference_info_removed)
+ },
+ getString(R.string.dialog_delete)
+ )
+
+ deleteConferenceInfoDialog?.show()
+ }
+ }
+
+ binding.setBackClickListener {
+ goBack()
+ }
+
+ binding.setNewConferenceClickListener {
+ navigateToConferenceScheduling()
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceSchedulingViewModel.kt b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceSchedulingViewModel.kt
new file mode 100644
index 000000000..2f5ca4a79
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceSchedulingViewModel.kt
@@ -0,0 +1,277 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.activities.main.conference.viewmodels
+
+import androidx.lifecycle.MediatorLiveData
+import androidx.lifecycle.MutableLiveData
+import java.util.*
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.activities.main.conference.data.ConferenceSchedulingParticipantData
+import org.linphone.activities.main.conference.data.Duration
+import org.linphone.activities.main.conference.data.TimeZoneData
+import org.linphone.contact.ContactsSelectionViewModel
+import org.linphone.core.*
+import org.linphone.core.tools.Log
+import org.linphone.utils.Event
+import org.linphone.utils.TimestampUtils
+
+class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
+ val subject = MutableLiveData()
+ val description = MutableLiveData()
+
+ val scheduleForLater = MutableLiveData()
+
+ val formattedDate = MutableLiveData()
+ val formattedTime = MutableLiveData()
+
+ val isEncrypted = MutableLiveData()
+
+ val sendInviteViaChat = MutableLiveData()
+ val sendInviteViaEmail = MutableLiveData()
+
+ val participantsData = MutableLiveData>()
+
+ val address = MutableLiveData()
+
+ val conferenceCreationInProgress = MutableLiveData()
+
+ val conferenceCreationCompletedEvent: MutableLiveData>> by lazy {
+ MutableLiveData>>()
+ }
+
+ val continueEnabled: MediatorLiveData = MediatorLiveData()
+
+ var timeZone = MutableLiveData()
+ val timeZones: List = computeTimeZonesList()
+
+ var duration = MutableLiveData()
+ val durationList: List = computeDurationList()
+
+ var dateTimestamp: Long = System.currentTimeMillis()
+ var hour: Int = 0
+ var minutes: Int = 0
+
+ private val conferenceScheduler = coreContext.core.createConferenceScheduler()
+
+ private val chatRoomListener = object : ChatRoomListenerStub() {
+ override fun onStateChanged(room: ChatRoom, state: ChatRoom.State) {
+ if (state == ChatRoom.State.Created) {
+ Log.i("[Conference Creation] Chat room created")
+ room.removeListener(this)
+ } else if (state == ChatRoom.State.CreationFailed) {
+ Log.e("[Conference Creation] Group chat room creation has failed !")
+ room.removeListener(this)
+ }
+ }
+ }
+
+ private val listener = object : ConferenceSchedulerListenerStub() {
+ override fun onStateChanged(
+ conferenceScheduler: ConferenceScheduler,
+ state: ConferenceSchedulerState
+ ) {
+ Log.i("[Conference Creation] Conference scheduler state is $state")
+ if (state == ConferenceSchedulerState.Ready) {
+ val conferenceAddress = conferenceScheduler.info?.uri
+ Log.i("[Conference Creation] Conference info created, address will be ${conferenceAddress?.asStringUriOnly()}")
+ conferenceAddress ?: return
+
+ address.value = conferenceAddress!!
+
+ if (sendInviteViaChat.value == true) {
+ // Send conference info even when conf is not scheduled for later
+ // as the conference server doesn't invite participants automatically
+ val chatRoomParams = coreContext.core.createDefaultChatRoomParams()
+ chatRoomParams.backend = ChatRoomBackend.FlexisipChat
+ chatRoomParams.isGroupEnabled = false
+ chatRoomParams.isEncryptionEnabled = true
+ chatRoomParams.subject = subject.value
+ conferenceScheduler.sendInvitations(chatRoomParams)
+ } else {
+ conferenceCreationInProgress.value = false
+ conferenceCreationCompletedEvent.value = Event(Pair(conferenceAddress.asStringUriOnly(), conferenceScheduler.info?.subject))
+ }
+ }
+ }
+
+ override fun onInvitationsSent(
+ conferenceScheduler: ConferenceScheduler,
+ failedInvitations: Array?
+ ) {
+ Log.i("[Conference Creation] Conference information successfully sent to all participants")
+ conferenceCreationInProgress.value = false
+
+ if (failedInvitations?.isNotEmpty() == true) {
+ for (address in failedInvitations) {
+ Log.e("[Conference Creation] Conference information wasn't sent to participant ${address.asStringUriOnly()}")
+ }
+ onMessageToNotifyEvent.value = Event(R.string.conference_schedule_info_not_sent_to_participant)
+ }
+
+ val conferenceAddress = conferenceScheduler.info?.uri
+ if (conferenceAddress == null) {
+ Log.e("[Conference Creation] Conference address is null!")
+ } else {
+ conferenceCreationCompletedEvent.value = Event(Pair(conferenceAddress.asStringUriOnly(), conferenceScheduler.info?.subject))
+ }
+ }
+ }
+
+ init {
+ sipContactsSelected.value = true
+
+ subject.value = ""
+ scheduleForLater.value = false
+ isEncrypted.value = false
+ sendInviteViaChat.value = true
+ sendInviteViaEmail.value = false
+
+ timeZone.value = timeZones.find {
+ it.id == TimeZone.getDefault().id
+ }
+ duration.value = durationList.find {
+ it.value == 3600
+ }
+
+ continueEnabled.value = false
+ continueEnabled.addSource(subject) {
+ continueEnabled.value = allMandatoryFieldsFilled()
+ }
+ continueEnabled.addSource(scheduleForLater) {
+ continueEnabled.value = allMandatoryFieldsFilled()
+ }
+ continueEnabled.addSource(formattedDate) {
+ continueEnabled.value = allMandatoryFieldsFilled()
+ }
+ continueEnabled.addSource(formattedTime) {
+ continueEnabled.value = allMandatoryFieldsFilled()
+ }
+
+ conferenceScheduler.addListener(listener)
+ }
+
+ override fun onCleared() {
+ conferenceScheduler.removeListener(listener)
+ participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy)
+
+ super.onCleared()
+ }
+
+ fun toggleSchedule() {
+ scheduleForLater.value = scheduleForLater.value == false
+ }
+
+ fun setDate(d: Long) {
+ dateTimestamp = d
+ formattedDate.value = TimestampUtils.dateToString(dateTimestamp, false)
+ }
+
+ fun setTime(h: Int, m: Int) {
+ hour = h
+ minutes = m
+ formattedTime.value = TimestampUtils.timeToString(hour, minutes)
+ }
+
+ fun updateEncryption(enable: Boolean) {
+ isEncrypted.value = enable
+ }
+
+ fun computeParticipantsData() {
+ participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy)
+ val list = arrayListOf()
+
+ for (address in selectedAddresses.value.orEmpty()) {
+ val data = ConferenceSchedulingParticipantData(address, isEncrypted.value == true)
+ list.add(data)
+ }
+
+ participantsData.value = list
+ }
+
+ fun createConference() {
+ val participantsCount = selectedAddresses.value.orEmpty().size
+ if (participantsCount == 0) {
+ Log.e("[Conference Creation] Couldn't create conference without any participant!")
+ return
+ }
+
+ conferenceCreationInProgress.value = true
+ val core = coreContext.core
+ val participants = arrayOfNulls(selectedAddresses.value.orEmpty().size)
+ selectedAddresses.value?.toArray(participants)
+ val localAddress = core.defaultAccount?.params?.identityAddress
+
+ // TODO: Temporary workaround for chat room, to be removed once we can get matching chat room from conference
+ /*val chatRoomParams = core.createDefaultChatRoomParams()
+ chatRoomParams.backend = ChatRoomBackend.FlexisipChat
+ chatRoomParams.enableGroup(true)
+ chatRoomParams.subject = subject.value
+ val chatRoom = core.createChatRoom(chatRoomParams, localAddress, participants)
+ if (chatRoom == null) {
+ Log.e("[Conference Creation] Failed to create a chat room with same subject & participants as for conference")
+ } else {
+ Log.i("[Conference Creation] Creating chat room with same subject [${subject.value}] & participants as for conference")
+ chatRoom.addListener(chatRoomListener)
+ }*/
+ // END OF TODO
+
+ val conferenceInfo = Factory.instance().createConferenceInfo()
+ conferenceInfo.organizer = localAddress
+ conferenceInfo.subject = subject.value
+ conferenceInfo.description = description.value
+ conferenceInfo.setParticipants(participants)
+ if (scheduleForLater.value == true) {
+ val startTime = getConferenceStartTimestamp()
+ conferenceInfo.dateTime = startTime
+ val duration = duration.value?.value ?: 0
+ conferenceInfo.duration = duration
+ }
+ conferenceScheduler.info = conferenceInfo // Will trigger the conference creation automatically
+ }
+
+ private fun computeTimeZonesList(): List {
+ return TimeZone.getAvailableIDs().map { id -> TimeZoneData(TimeZone.getTimeZone(id)) }.toList().sorted()
+ }
+
+ private fun computeDurationList(): List {
+ // Duration value is in minutes as according to conferenceInfo.setDuration() doc
+ return arrayListOf(Duration(30, "30min"), Duration(60, "1h"), Duration(120, "2h"))
+ }
+
+ private fun allMandatoryFieldsFilled(): Boolean {
+ return !subject.value.isNullOrEmpty() &&
+ (
+ scheduleForLater.value == false ||
+ (
+ !formattedDate.value.isNullOrEmpty() &&
+ !formattedTime.value.isNullOrEmpty()
+ )
+ )
+ }
+
+ private fun getConferenceStartTimestamp(): Long {
+ val calendar = Calendar.getInstance(TimeZone.getTimeZone(timeZone.value?.id ?: TimeZone.getDefault().id))
+ calendar.timeInMillis = dateTimestamp
+ calendar.set(Calendar.HOUR_OF_DAY, hour)
+ calendar.set(Calendar.MINUTE, minutes)
+ return calendar.timeInMillis / 1000 // Linphone expects a time_t (so in seconds)
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceWaitingRoomViewModel.kt b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceWaitingRoomViewModel.kt
new file mode 100644
index 000000000..eeed7e054
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceWaitingRoomViewModel.kt
@@ -0,0 +1,254 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.activities.main.conference.viewmodels
+
+import android.Manifest
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.core.*
+import org.linphone.core.tools.Log
+import org.linphone.utils.AudioRouteUtils
+import org.linphone.utils.Event
+import org.linphone.utils.PermissionHelper
+
+class ConferenceWaitingRoomViewModel : ViewModel() {
+ val subject = MutableLiveData()
+
+ val isMicrophoneMuted = MutableLiveData()
+
+ val audioRoutesEnabled = MutableLiveData()
+
+ val audioRoutesSelected = MutableLiveData()
+
+ val isSpeakerSelected = MutableLiveData()
+
+ val isBluetoothHeadsetSelected = MutableLiveData()
+
+ val isVideoAvailable = MutableLiveData()
+
+ val isVideoEnabled = MutableLiveData()
+
+ val isSwitchCameraAvailable = MutableLiveData()
+
+ val joinInProgress = MutableLiveData()
+
+ val askPermissionEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val cancelConferenceJoiningEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val joinConferenceEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val leaveWaitingRoomEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ private val callParams: CallParams = coreContext.core.createCallParams(null)!!
+
+ private val listener: CoreListenerStub = object : CoreListenerStub() {
+ override fun onAudioDevicesListUpdated(core: Core) {
+ Log.i("[Conference Waiting Room] Audio devices list updated")
+ onAudioDevicesListUpdated()
+ }
+
+ override fun onCallStateChanged(
+ core: Core,
+ call: Call,
+ state: Call.State?,
+ message: String
+ ) {
+ if (state == Call.State.Connected) {
+ Log.i("[Conference Waiting Room] Call is now connected, leaving waiting room fragment")
+ leaveWaitingRoomEvent.value = Event(true)
+ }
+ }
+ }
+
+ init {
+ val core = coreContext.core
+ core.addListener(listener)
+
+ callParams.isMicEnabled = PermissionHelper.get().hasRecordAudioPermission()
+ Log.i("[Conference Waiting Room] Microphone will be ${if (callParams.isMicEnabled) "enabled" else "muted"}")
+ updateMicState()
+
+ isVideoAvailable.value = core.isVideoCaptureEnabled || core.isVideoPreviewEnabled
+ callParams.isVideoEnabled = core.videoActivationPolicy.automaticallyInitiate
+ Log.i("[Conference Waiting Room] Video will be ${if (callParams.isVideoEnabled) "enabled" else "disabled"}")
+ updateVideoState()
+
+ if (AudioRouteUtils.isBluetoothAudioRouteAvailable()) {
+ setBluetoothAudioRoute()
+ } else if (isVideoAvailable.value == true && isVideoEnabled.value == true) {
+ setSpeakerAudioRoute()
+ } else {
+ setEarpieceAudioRoute()
+ }
+ updateAudioRouteState()
+ }
+
+ override fun onCleared() {
+ coreContext.core.removeListener(listener)
+
+ super.onCleared()
+ }
+
+ fun cancel() {
+ cancelConferenceJoiningEvent.value = Event(true)
+ }
+
+ fun start() {
+ joinInProgress.value = true
+ joinConferenceEvent.value = Event(callParams)
+ }
+
+ fun toggleMuteMicrophone() {
+ if (!PermissionHelper.get().hasRecordAudioPermission()) {
+ askPermissionEvent.value = Event(Manifest.permission.RECORD_AUDIO)
+ return
+ }
+
+ callParams.isMicEnabled = !callParams.isMicEnabled
+ Log.i("[Conference Waiting Room] Microphone will be ${if (callParams.isMicEnabled) "enabled" else "muted"}")
+ updateMicState()
+ }
+
+ fun enableMic() {
+ Log.i("[Conference Waiting Room] Microphone will be enabled")
+ callParams.isMicEnabled = true
+ updateMicState()
+ }
+
+ fun toggleSpeaker() {
+ if (isSpeakerSelected.value == true) {
+ setEarpieceAudioRoute()
+ } else {
+ setSpeakerAudioRoute()
+ }
+ }
+
+ fun toggleAudioRoutesMenu() {
+ audioRoutesSelected.value = audioRoutesSelected.value != true
+ }
+
+ fun setBluetoothAudioRoute() {
+ Log.i("[Conference Waiting Room] Set default output audio device to Bluetooth")
+ callParams.outputAudioDevice = coreContext.core.audioDevices.find {
+ it.type == AudioDevice.Type.Bluetooth && it.hasCapability(AudioDevice.Capabilities.CapabilityPlay)
+ }
+ callParams.inputAudioDevice = coreContext.core.audioDevices.find {
+ it.type == AudioDevice.Type.Bluetooth && it.hasCapability(AudioDevice.Capabilities.CapabilityRecord)
+ }
+ updateAudioRouteState()
+ }
+
+ fun setSpeakerAudioRoute() {
+ Log.i("[Conference Waiting Room] Set default output audio device to Speaker")
+ callParams.outputAudioDevice = coreContext.core.audioDevices.find {
+ it.type == AudioDevice.Type.Speaker && it.hasCapability(AudioDevice.Capabilities.CapabilityPlay)
+ }
+ callParams.inputAudioDevice = coreContext.core.audioDevices.find {
+ it.type == AudioDevice.Type.Microphone && it.hasCapability(AudioDevice.Capabilities.CapabilityRecord)
+ }
+ updateAudioRouteState()
+ }
+
+ fun setEarpieceAudioRoute() {
+ Log.i("[Conference Waiting Room] Set default output audio device to Earpiece")
+ callParams.outputAudioDevice = coreContext.core.audioDevices.find {
+ it.type == AudioDevice.Type.Earpiece && it.hasCapability(AudioDevice.Capabilities.CapabilityPlay)
+ }
+ callParams.inputAudioDevice = coreContext.core.audioDevices.find {
+ it.type == AudioDevice.Type.Microphone && it.hasCapability(AudioDevice.Capabilities.CapabilityRecord)
+ }
+ updateAudioRouteState()
+ }
+
+ fun toggleVideo() {
+ if (!PermissionHelper.get().hasCameraPermission()) {
+ askPermissionEvent.value = Event(Manifest.permission.CAMERA)
+ return
+ }
+ callParams.isVideoEnabled = !callParams.isVideoEnabled
+ Log.i("[Conference Waiting Room] Video will be ${if (callParams.isVideoEnabled) "enabled" else "disabled"}")
+ updateVideoState()
+ }
+
+ fun enableVideo() {
+ Log.i("[Conference Waiting Room] Video will be enabled")
+ callParams.isVideoEnabled = true
+ updateVideoState()
+ }
+
+ fun switchCamera() {
+ Log.i("[Conference Waiting Room] Switching camera")
+ coreContext.switchCamera()
+ }
+
+ private fun updateMicState() {
+ isMicrophoneMuted.value = !callParams.isMicEnabled
+ }
+
+ private fun onAudioDevicesListUpdated() {
+ val bluetoothDeviceAvailable = AudioRouteUtils.isBluetoothAudioRouteAvailable()
+ audioRoutesEnabled.value = bluetoothDeviceAvailable
+
+ if (!bluetoothDeviceAvailable) {
+ audioRoutesSelected.value = false
+ Log.w("[Conference Waiting Room] Bluetooth device no longer available, switching back to default microphone & earpiece/speaker")
+ if (isBluetoothHeadsetSelected.value == true) {
+ for (audioDevice in coreContext.core.audioDevices) {
+ if (isVideoEnabled.value == true) {
+ if (audioDevice.type == AudioDevice.Type.Speaker) {
+ callParams.outputAudioDevice = audioDevice
+ }
+ } else {
+ if (audioDevice.type == AudioDevice.Type.Earpiece) {
+ callParams.outputAudioDevice = audioDevice
+ }
+ }
+ if (audioDevice.type == AudioDevice.Type.Microphone) {
+ callParams.inputAudioDevice = audioDevice
+ }
+ }
+ }
+ }
+
+ updateAudioRouteState()
+ }
+
+ private fun updateAudioRouteState() {
+ val outputDeviceType = callParams.outputAudioDevice?.type
+ isSpeakerSelected.value = outputDeviceType == AudioDevice.Type.Speaker
+ isBluetoothHeadsetSelected.value = outputDeviceType == AudioDevice.Type.Bluetooth
+ }
+
+ private fun updateVideoState() {
+ isVideoEnabled.value = callParams.isVideoEnabled
+ isSwitchCameraAvailable.value = callParams.isVideoEnabled && coreContext.showSwitchCameraButton()
+ coreContext.core.isVideoPreviewEnabled = callParams.isVideoEnabled
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ScheduledConferencesViewModel.kt b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ScheduledConferencesViewModel.kt
new file mode 100644
index 000000000..5e2355d44
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ScheduledConferencesViewModel.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.activities.main.conference.viewmodels
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.activities.main.conference.data.ScheduledConferenceData
+import org.linphone.core.ConferenceInfo
+import org.linphone.core.Core
+import org.linphone.core.CoreListenerStub
+import org.linphone.core.tools.Log
+
+class ScheduledConferencesViewModel : ViewModel() {
+ val conferences = MutableLiveData>()
+
+ private val listener = object : CoreListenerStub() {
+ override fun onConferenceInfoReceived(core: Core, conferenceInfo: ConferenceInfo) {
+ Log.i("[Scheduled Conferences] New conference info received")
+ val conferencesList = arrayListOf()
+ conferencesList.addAll(conferences.value.orEmpty())
+ val data = ScheduledConferenceData(conferenceInfo)
+ conferencesList.add(data)
+ conferences.value = conferencesList
+ }
+ }
+
+ init {
+ coreContext.core.addListener(listener)
+ computeConferenceInfoList()
+ }
+
+ override fun onCleared() {
+ coreContext.core.removeListener(listener)
+ conferences.value.orEmpty().forEach(ScheduledConferenceData::destroy)
+ super.onCleared()
+ }
+
+ fun deleteConferenceInfo(data: ScheduledConferenceData) {
+ val conferenceInfoList = arrayListOf()
+
+ conferenceInfoList.addAll(conferences.value.orEmpty())
+ conferenceInfoList.remove(data)
+
+ data.delete()
+ data.destroy()
+ conferences.value = conferenceInfoList
+ }
+
+ private fun computeConferenceInfoList() {
+ conferences.value.orEmpty().forEach(ScheduledConferenceData::destroy)
+
+ val conferencesList = arrayListOf()
+
+ val now = System.currentTimeMillis() / 1000 // Linphone uses time_t in seconds
+ val oneHourAgo = now - 3600 // Show all conferences from 1 hour ago and forward
+ for (conferenceInfo in coreContext.core.getConferenceInformationListAfterTime(oneHourAgo)) {
+ val data = ScheduledConferenceData(conferenceInfo)
+ conferencesList.add(data)
+ }
+
+ conferences.value = conferencesList
+ Log.i("[Scheduled Conferences] Found ${conferencesList.size} future conferences")
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt
index ac738abd4..552245bf6 100644
--- a/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt
@@ -138,7 +138,7 @@ class ContactEditorFragment : GenericFragment(), S
Log.i("[Contact Editor] WRITE_CONTACTS permission granted")
} else {
Log.w("[Contact Editor] WRITE_CONTACTS permission denied")
- (requireActivity() as MainActivity).showSnackBar(R.string.contact_editor_write_permission_denied)
+ (activity as MainActivity).showSnackBar(R.string.contact_editor_write_permission_denied)
goBack()
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt
index cf7384fae..dff171a33 100644
--- a/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt
@@ -71,7 +71,6 @@ class DetailContactFragment : GenericFragment() {
val contact = sharedViewModel.selectedContact.value
if (contact == null) {
Log.e("[Contact] Contact is null, aborting!")
- // (activity as MainActivity).showSnackBar(R.string.error)
goBack()
return
}
@@ -140,7 +139,7 @@ class DetailContactFragment : GenericFragment() {
confirmContactRemoval()
}
- viewModel.onErrorEvent.observe(
+ viewModel.onMessageToNotifyEvent.observe(
viewLifecycleOwner
) {
it.consume { messageResourceId ->
diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt
index bc419400f..540dad03c 100644
--- a/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt
@@ -138,7 +138,6 @@ class MasterContactsFragment : MasterFragment = MutableLiveData()
override val displayName: MutableLiveData = MutableLiveData()
override val securityLevel: MutableLiveData = MutableLiveData()
@@ -91,7 +91,7 @@ class ContactViewModel(val contactInternal: Contact) : ErrorReportingViewModel()
} else if (state == ChatRoom.State.CreationFailed) {
Log.e("[Contact Detail] Group chat room creation has failed !")
waitForChatRoomCreation.value = false
- onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack)
+ onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
}
}
}
@@ -115,7 +115,7 @@ class ContactViewModel(val contactInternal: Contact) : ErrorReportingViewModel()
} else {
waitForChatRoomCreation.value = false
Log.e("[Contact Detail] Couldn't create chat room with address $address")
- onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack)
+ onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt b/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt
index 04791ca56..de60f66b9 100644
--- a/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt
@@ -31,6 +31,7 @@ import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
import android.view.View
+import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.transition.MaterialSharedAxis
@@ -43,6 +44,7 @@ import org.linphone.activities.main.dialer.viewmodels.DialerViewModel
import org.linphone.activities.main.fragments.SecureFragment
import org.linphone.activities.main.viewmodels.DialogViewModel
import org.linphone.activities.main.viewmodels.SharedMainViewModel
+import org.linphone.activities.navigateToConferenceScheduling
import org.linphone.activities.navigateToConfigFileViewer
import org.linphone.activities.navigateToContacts
import org.linphone.compatibility.Compatibility
@@ -103,11 +105,17 @@ class DialerFragment : SecureFragment() {
navigateToContacts(viewModel.enteredUri.value)
}
+ binding.setNewConferenceClickListener {
+ sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.conferenceSchedulingFragment)
+ navigateToConferenceScheduling()
+ }
+
binding.setTransferCallClickListener {
if (viewModel.transferCall()) {
// Transfer has been consumed, otherwise it might have been a "bis" use
sharedViewModel.pendingCallTransfer = false
viewModel.transferVisibility.value = false
+ coreContext.onCallStarted()
}
}
@@ -147,6 +155,14 @@ class DialerFragment : SecureFragment() {
}
}
+ viewModel.onMessageToNotifyEvent.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume { id ->
+ Toast.makeText(requireContext(), id, Toast.LENGTH_SHORT).show()
+ }
+ }
+
if (corePreferences.firstStart) {
Log.w("[Dialer] First start detected, wait for assistant to be finished to check for update & request permissions")
return
diff --git a/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt b/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt
index 964399d6b..45874eb05 100644
--- a/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt
@@ -47,6 +47,8 @@ class DialerViewModel : LogsUploadViewModel() {
val autoInitiateVideoCalls = MutableLiveData()
+ val scheduleConferenceAvailable = MutableLiveData()
+
val updateAvailableEvent: MutableLiveData> by lazy {
MutableLiveData>()
}
@@ -136,6 +138,7 @@ class DialerViewModel : LogsUploadViewModel() {
transferVisibility.value = false
showSwitchCamera.value = coreContext.showSwitchCameraButton()
+ scheduleConferenceAvailable.value = LinphoneUtils.isRemoteConferencingAvailable()
}
override fun onCleared() {
@@ -195,7 +198,13 @@ class DialerViewModel : LogsUploadViewModel() {
fun transferCall(): Boolean {
val addressToCall = enteredUri.value.orEmpty()
return if (addressToCall.isNotEmpty()) {
- coreContext.transferCallTo(addressToCall)
+ onMessageToNotifyEvent.value = Event(
+ if (coreContext.transferCallTo(addressToCall)) {
+ org.linphone.R.string.dialer_transfer_succeded
+ } else {
+ org.linphone.R.string.dialer_transfer_failed
+ }
+ )
eraseAll()
true
} else {
diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/AudioViewerFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/AudioViewerFragment.kt
index 1f6877079..8dd25f62d 100644
--- a/app/src/main/java/org/linphone/activities/main/files/fragments/AudioViewerFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/files/fragments/AudioViewerFragment.kt
@@ -48,7 +48,6 @@ class AudioViewerFragment : GenericViewerFragment : SecureFragment()
val content = sharedViewModel.contentToOpen.value
if (content == null) {
Log.e("[Generic Viewer] Content is null, aborting!")
- // (activity as MainActivity).showSnackBar(R.string.error)
findNavController().navigateUp()
return
}
diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/ImageViewerFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/ImageViewerFragment.kt
index daae3d5aa..2bab58d08 100644
--- a/app/src/main/java/org/linphone/activities/main/files/fragments/ImageViewerFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/files/fragments/ImageViewerFragment.kt
@@ -44,7 +44,6 @@ class ImageViewerFragment : GenericViewerFragment()
val content = sharedViewModel.contentToOpen.value
if (content == null) {
Log.e("[PDF Viewer] Content is null, aborting!")
- // (activity as MainActivity).showSnackBar(R.string.error)
findNavController().navigateUp()
return
}
diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/TextViewerFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/TextViewerFragment.kt
index ea81e9124..470ce50c1 100644
--- a/app/src/main/java/org/linphone/activities/main/files/fragments/TextViewerFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/files/fragments/TextViewerFragment.kt
@@ -42,7 +42,6 @@ class TextViewerFragment : GenericViewerFragment(
val content = sharedViewModel.contentToOpen.value
if (content == null) {
Log.e("[Text Viewer] Content is null, aborting!")
- // (activity as MainActivity).showSnackBar(R.string.error)
findNavController().navigateUp()
return
}
diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/VideoViewerFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/VideoViewerFragment.kt
index f0efad396..f82711429 100644
--- a/app/src/main/java/org/linphone/activities/main/files/fragments/VideoViewerFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/files/fragments/VideoViewerFragment.kt
@@ -46,7 +46,6 @@ class VideoViewerFragment : GenericViewerFragment.
- */
-package org.linphone.activities.main.history.data
-
-import java.text.SimpleDateFormat
-import java.util.*
-import org.linphone.R
-import org.linphone.contact.GenericContactData
-import org.linphone.core.Call
-import org.linphone.core.CallLog
-import org.linphone.utils.TimestampUtils
-
-class CallLogData(callLog: CallLog) : GenericContactData(callLog.remoteAddress) {
- val statusIconResource: Int by lazy {
- if (callLog.dir == Call.Dir.Incoming) {
- if (callLog.status == Call.Status.Missed) {
- R.drawable.call_status_missed
- } else {
- R.drawable.call_status_incoming
- }
- } else {
- R.drawable.call_status_outgoing
- }
- }
-
- val iconContentDescription: Int by lazy {
- if (callLog.dir == Call.Dir.Incoming) {
- if (callLog.status == Call.Status.Missed) {
- R.string.content_description_missed_call
- } else {
- R.string.content_description_incoming_call
- }
- } else {
- R.string.content_description_outgoing_call
- }
- }
-
- val directionIconResource: Int by lazy {
- if (callLog.dir == Call.Dir.Incoming) {
- if (callLog.status == Call.Status.Missed) {
- R.drawable.call_missed
- } else {
- R.drawable.call_incoming
- }
- } else {
- R.drawable.call_outgoing
- }
- }
-
- val duration: String by lazy {
- val dateFormat = SimpleDateFormat(if (callLog.duration >= 3600) "HH:mm:ss" else "mm:ss", Locale.getDefault())
- val cal = Calendar.getInstance()
- cal[0, 0, 0, 0, 0] = callLog.duration
- dateFormat.format(cal.time)
- }
-
- val date: String by lazy {
- TimestampUtils.toString(callLog.startDate, shortDate = false, hideYear = false)
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/main/history/data/GroupedCallLogData.kt b/app/src/main/java/org/linphone/activities/main/history/data/GroupedCallLogData.kt
index ea76e8fe8..e00a9c5dd 100644
--- a/app/src/main/java/org/linphone/activities/main/history/data/GroupedCallLogData.kt
+++ b/app/src/main/java/org/linphone/activities/main/history/data/GroupedCallLogData.kt
@@ -19,14 +19,18 @@
*/
package org.linphone.activities.main.history.data
+import org.linphone.activities.main.history.viewmodels.CallLogViewModel
import org.linphone.core.CallLog
class GroupedCallLogData(callLog: CallLog) {
var lastCallLog: CallLog = callLog
val callLogs = arrayListOf(callLog)
- val lastCallLogData = CallLogData(lastCallLog)
+ val lastCallLogViewModel: CallLogViewModel
+ get() {
+ return CallLogViewModel(lastCallLog)
+ }
fun destroy() {
- lastCallLogData.destroy()
+ lastCallLogViewModel.destroy()
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt
index 825921125..f444e86e2 100644
--- a/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt
@@ -57,7 +57,6 @@ class DetailCallLogFragment : GenericFragment() {
val callLogGroup = sharedViewModel.selectedCallLogGroup.value
if (callLogGroup == null) {
Log.e("[History] Call log group is null, aborting!")
- // (activity as MainActivity).showSnackBar(R.string.error)
findNavController().navigateUp()
return
}
@@ -70,7 +69,7 @@ class DetailCallLogFragment : GenericFragment() {
useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false
- viewModel.addRelatedCallLogs(callLogGroup.callLogs)
+ viewModel.relatedCallLogs.value = callLogGroup.callLogs
binding.setBackClickListener {
goBack()
@@ -134,7 +133,7 @@ class DetailCallLogFragment : GenericFragment() {
}
}
- viewModel.onErrorEvent.observe(
+ viewModel.onMessageToNotifyEvent.observe(
viewLifecycleOwner
) {
it.consume { messageResourceId ->
diff --git a/app/src/main/java/org/linphone/activities/main/history/fragments/DetailConferenceCallLogFragment.kt b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailConferenceCallLogFragment.kt
new file mode 100644
index 000000000..efcd9b5fe
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailConferenceCallLogFragment.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.activities.main.history.fragments
+
+import android.os.Bundle
+import android.view.View
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import org.linphone.R
+import org.linphone.activities.*
+import org.linphone.activities.main.*
+import org.linphone.activities.main.history.viewmodels.CallLogViewModel
+import org.linphone.activities.main.history.viewmodels.CallLogViewModelFactory
+import org.linphone.activities.main.viewmodels.SharedMainViewModel
+import org.linphone.core.tools.Log
+import org.linphone.databinding.HistoryConfDetailFragmentBinding
+import org.linphone.utils.Event
+
+class DetailConferenceCallLogFragment : GenericFragment() {
+ private lateinit var viewModel: CallLogViewModel
+ private lateinit var sharedViewModel: SharedMainViewModel
+
+ override fun getLayoutId(): Int = R.layout.history_conf_detail_fragment
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.lifecycleOwner = viewLifecycleOwner
+
+ sharedViewModel = requireActivity().run {
+ ViewModelProvider(this)[SharedMainViewModel::class.java]
+ }
+ binding.sharedMainViewModel = sharedViewModel
+
+ val callLogGroup = sharedViewModel.selectedCallLogGroup.value
+ if (callLogGroup == null) {
+ Log.e("[History] Call log group is null, aborting!")
+ findNavController().navigateUp()
+ return
+ }
+
+ viewModel = ViewModelProvider(
+ this,
+ CallLogViewModelFactory(callLogGroup.lastCallLog)
+ )[CallLogViewModel::class.java]
+ binding.viewModel = viewModel
+
+ useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false
+
+ viewModel.relatedCallLogs.value = callLogGroup.callLogs
+
+ binding.setBackClickListener {
+ goBack()
+ }
+
+ viewModel.onMessageToNotifyEvent.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume { messageResourceId ->
+ (activity as MainActivity).showSnackBar(messageResourceId)
+ }
+ }
+ }
+
+ override fun goBack() {
+ if (sharedViewModel.isSlidingPaneSlideable.value == true) {
+ sharedViewModel.closeSlidingPaneEvent.value = Event(true)
+ } else {
+ navigateToEmptyCallHistory()
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt b/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt
index bbd2e0cfa..389a74be9 100644
--- a/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt
@@ -35,6 +35,7 @@ import com.google.android.material.transition.MaterialSharedAxis
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
+import org.linphone.activities.*
import org.linphone.activities.clearDisplayedCallHistory
import org.linphone.activities.main.fragments.MasterFragment
import org.linphone.activities.main.history.adapters.CallLogsListAdapter
@@ -44,6 +45,7 @@ import org.linphone.activities.main.viewmodels.DialogViewModel
import org.linphone.activities.main.viewmodels.SharedMainViewModel
import org.linphone.activities.main.viewmodels.TabsViewModel
import org.linphone.activities.navigateToCallHistory
+import org.linphone.activities.navigateToConferenceCallHistory
import org.linphone.activities.navigateToDialer
import org.linphone.core.tools.Log
import org.linphone.databinding.HistoryMasterFragmentBinding
@@ -125,7 +127,6 @@ class MasterCallLogsFragment : MasterFragment
- if (listViewModel.missedCallLogsSelected.value == false) {
- adapter.submitList(callLogs)
- }
- }
-
- listViewModel.missedCallLogs.observe(
- viewLifecycleOwner
- ) { callLogs ->
- if (listViewModel.missedCallLogsSelected.value == true) {
- adapter.submitList(callLogs)
- }
- }
-
- listViewModel.missedCallLogsSelected.observe(
- viewLifecycleOwner
- ) {
- if (it) {
- adapter.submitList(listViewModel.missedCallLogs.value)
- } else {
- adapter.submitList(listViewModel.callLogs.value)
- }
+ adapter.submitList(callLogs)
}
listViewModel.contactsUpdatedEvent.observe(
@@ -230,24 +211,29 @@ class MasterCallLogsFragment : MasterFragment
sharedViewModel.selectedCallLogGroup.value = callLog
- navigateToCallHistory(binding.slidingPane)
+ if (callLog.lastCallLog.wasConference()) {
+ navigateToConferenceCallHistory(binding.slidingPane)
+ } else {
+ navigateToCallHistory(binding.slidingPane)
+ }
}
}
adapter.startCallToEvent.observe(
- viewLifecycleOwner
+ viewLifecycleOwner,
) {
it.consume { callLogGroup ->
val remoteAddress = callLogGroup.lastCallLog.remoteAddress
- if (coreContext.core.callsNb > 0) {
+ val conferenceInfo = coreContext.core.findConferenceInformationFromUri(remoteAddress)
+ if (conferenceInfo != null) {
+ navigateToConferenceWaitingRoom(remoteAddress.asStringUriOnly(), conferenceInfo.subject)
+ } else if (coreContext.core.callsNb > 0) {
Log.i("[History] Starting dialer with pre-filled URI ${remoteAddress.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}")
- sharedViewModel.updateDialerAnimationsBasedOnDestination.value =
- Event(R.id.masterCallLogsFragment)
+ sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.masterCallLogsFragment)
val args = Bundle()
args.putString("URI", remoteAddress.asStringUriOnly())
args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer)
- // If auto start call setting is enabled, ignore it
- args.putBoolean("SkipAutoCallStart", true)
+ args.putBoolean("SkipAutoCallStart", true) // If auto start call setting is enabled, ignore it
navigateToDialer(args)
} else {
val localAddress = callLogGroup.lastCallLog.localAddress
@@ -256,13 +242,6 @@ class MasterCallLogsFragment : MasterFragment= 3600) "HH:mm:ss" else "mm:ss", Locale.getDefault())
+ val cal = Calendar.getInstance()
+ cal[0, 0, 0, 0, 0] = callLog.duration
+ dateFormat.format(cal.time)
+ }
+
+ val date: String by lazy {
+ TimestampUtils.toString(callLog.startDate, shortDate = false, hideYear = false)
+ }
+
val startCallEvent: MutableLiveData> by lazy {
MutableLiveData>()
}
@@ -61,17 +112,30 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
val secureChatAllowed = contact.value?.friend?.getPresenceModelForUriOrTel(peerSipUri)?.hasCapability(FriendCapability.LimeX3Dh) ?: false
- val relatedCallLogs = MutableLiveData>()
+ val relatedCallLogs = MutableLiveData>()
private val listener = object : CoreListenerStub() {
override fun onCallLogUpdated(core: Core, log: CallLog) {
if (callLog.remoteAddress.weakEqual(log.remoteAddress) && callLog.localAddress.weakEqual(log.localAddress)) {
Log.i("[History Detail] New call log for ${callLog.remoteAddress.asStringUriOnly()} with local address ${callLog.localAddress.asStringUriOnly()}")
- addRelatedCallLogs(arrayListOf(log))
+ val list = arrayListOf()
+ list.add(callLog)
+ list.addAll(relatedCallLogs.value.orEmpty())
+ relatedCallLogs.value = list
}
}
}
+ val isConferenceCallLog = callLog.wasConference()
+
+ val conferenceSubject = callLog.conferenceInfo?.subject
+ val conferenceParticipantsData = MutableLiveData>()
+ val conferenceTime = MutableLiveData()
+ val conferenceDate = MutableLiveData()
+
+ override val showGroupChatAvatar: Boolean
+ get() = isConferenceCallLog
+
private val chatRoomListener = object : ChatRoomListenerStub() {
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
if (state == ChatRoom.State.Created) {
@@ -80,7 +144,7 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
} else if (state == ChatRoom.State.CreationFailed) {
Log.e("[History Detail] Group chat room creation has failed !")
waitForChatRoomCreation.value = false
- onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack)
+ onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
}
}
}
@@ -89,17 +153,31 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
waitForChatRoomCreation.value = false
coreContext.core.addListener(listener)
+
+ val conferenceInfo = callLog.conferenceInfo
+ if (conferenceInfo != null) {
+ conferenceTime.value = TimestampUtils.timeToString(conferenceInfo.dateTime)
+ conferenceDate.value = if (TimestampUtils.isToday(conferenceInfo.dateTime)) {
+ AppUtils.getString(R.string.today)
+ } else {
+ TimestampUtils.toString(conferenceInfo.dateTime, onlyDate = true, shortDate = false, hideYear = false)
+ }
+ val list = arrayListOf