New call/conference UI

This commit is contained in:
Sylvain Berfini 2021-09-15 11:44:23 +02:00
parent 51cf7a6711
commit 6ef3dc288e
468 changed files with 13721 additions and 5940 deletions

View file

@ -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'

View file

@ -115,19 +115,11 @@
<activity android:name=".activities.assistant.AssistantActivity"
android:windowSoftInputMode="adjustResize"/>
<activity android:name=".activities.call.CallActivity"
<activity android:name=".activities.voip.CallActivity"
android:launchMode="singleTop"
android:resizeableActivity="true"
android:supportsPictureInPicture="true" />
<activity android:name=".activities.call.IncomingCallActivity"
android:launchMode="singleTop"
android:noHistory="true" />
<activity android:name=".activities.call.OutgoingCallActivity"
android:launchMode="singleTop"
android:noHistory="true" />
<activity
android:name=".activities.chat_bubble.ChatBubbleActivity"
android:allowEmbedded="true"

View file

@ -15,8 +15,10 @@
<entry name="nat_policy_ref" overwrite="true"></entry>
<entry name="realm" overwrite="true"></entry>
<entry name="conference_factory_uri" overwrite="true"></entry>
<entry name="audio_video_conference_factory_uri" overwrite="true"></entry>
<entry name="push_notification_allowed" overwrite="true">0</entry>
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">0</entry>
<entry name="rtp_bundle" overwrite="true">0</entry>
</section>
<section name="nat_policy_default_values">
<entry name="stun_server" overwrite="true"></entry>

View file

@ -15,8 +15,10 @@
<entry name="nat_policy_ref" overwrite="true">nat_policy_default_values</entry>
<entry name="realm" overwrite="true">sip.linphone.org</entry>
<entry name="conference_factory_uri" overwrite="true">sip:conference-factory@sip.linphone.org</entry>
<entry name="audio_video_conference_factory_uri" overwrite="true">sip:videoconference-factory2@sip.linphone.org</entry>
<entry name="push_notification_allowed" overwrite="true">1</entry>
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">1</entry>
<entry name="rtp_bundle" overwrite="true">1</entry>
</section>
<section name="nat_policy_default_values">
<entry name="stun_server" overwrite="true">stun.linphone.org</entry>

View file

@ -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

View file

@ -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() {

View file

@ -17,14 +17,13 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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)
}
}

View file

@ -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<AssistantPhoneAccountL
viewLifecycleOwner
) {
it.consume {
if (LinphoneApplication.coreContext.core.isEchoCancellerCalibrationRequired) {
if (coreContext.core.isEchoCancellerCalibrationRequired) {
navigateToEchoCancellerCalibration()
} else {
requireActivity().finish()

View file

@ -1,208 +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 <http://www.gnu.org/licenses/>.
*/
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)
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String>()
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<String>(permissionsRequiredList.size)
permissionsRequiredList.toArray(permissionsRequired)
requestPermissions(permissionsRequired, 0)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
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
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String>()
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<String>(permissionsRequiredList.size)
permissionsRequiredList.toArray(permissionsRequired)
requestPermissions(permissionsRequired, 0)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
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<FlexboxLayout>(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<FlexboxLayout>(R.id.numpad)?.translationX = -screenWidth
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Boolean>()
val isMeAdmin = MutableLiveData<Boolean>()
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)
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<CallControlsFragmentBinding>() {
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<out String>,
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<String>()
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<String>(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<FlexboxLayout>(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<FlexboxLayout>(R.id.numpad)?.translationX = -screenWidth
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<CallVideoFragmentBinding>() {
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)
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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 <T : ViewModel> create(modelClass: Class<T>): 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<Boolean>()
val isOutgoingEarlyMedia = MutableLiveData<Boolean>()
val callEndedEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val callConnectedEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
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
)
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<CallViewModel>()
val noActiveCall = MutableLiveData<Boolean>()
val callPausedByRemote = MutableLiveData<Boolean>()
val pausedCalls = MutableLiveData<ArrayList<CallViewModel>>()
val noMoreCallEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val callUpdateEvent: MutableLiveData<Event<Call>> by lazy {
MutableLiveData<Event<Call>>()
}
val askWriteExternalStoragePermissionEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
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<CallViewModel>()
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<CallViewModel>()
list.addAll(pausedCalls.value.orEmpty())
for (pausedCallViewModel in list) {
if (pausedCallViewModel.call == call) {
pausedCallViewModel.destroy()
list.remove(pausedCallViewModel)
break
}
}
pausedCalls.value = list
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Boolean>()
val isMeConferenceFocus = MutableLiveData<Boolean>()
val conferenceAddress = MutableLiveData<Address>()
val conferenceParticipants = MutableLiveData<List<ConferenceParticipantData>>()
val isInConference = MutableLiveData<Boolean>()
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<Address>()
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<Address>()
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<ConferenceParticipantData>()
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()
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Boolean>()
val isVideoPreviewHidden = MutableLiveData<Boolean>()
val isVideoPreviewResizedForPip = MutableLiveData<Boolean>()
val videoEnabled = MutableLiveData<Boolean>()
val proximitySensorEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
private val nonEarpieceOutputAudioDevice = MutableLiveData<Boolean>()
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
)
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Boolean>()
val isMuteMicrophoneEnabled = MutableLiveData<Boolean>()
val isSpeakerSelected = MutableLiveData<Boolean>()
val isBluetoothHeadsetSelected = MutableLiveData<Boolean>()
val isVideoAvailable = MutableLiveData<Boolean>()
val isVideoEnabled = MutableLiveData<Boolean>()
val isVideoUpdateInProgress = MutableLiveData<Boolean>()
val showSwitchCamera = MutableLiveData<Boolean>()
val isPauseEnabled = MutableLiveData<Boolean>()
val isRecording = MutableLiveData<Boolean>()
val isConferencingAvailable = MutableLiveData<Boolean>()
val unreadMessagesCount = MutableLiveData<Int>()
val numpadVisibility = MutableLiveData<Boolean>()
val optionsVisibility = MutableLiveData<Boolean>()
val audioRoutesSelected = MutableLiveData<Boolean>()
val audioRoutesEnabled = MutableLiveData<Boolean>()
val takeScreenshotEnabled = MutableLiveData<Boolean>()
val chatClickedEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val addCallClickedEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val transferCallClickedEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val askAudioRecordPermissionEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
val askCameraPermissionEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
val somethingClickedEvent = MutableLiveData<Event<Boolean>>()
val chatAllowed = !corePreferences.disableChat
private val vibrator = coreContext.context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
val chatUnreadCountTranslateY = MutableLiveData<Float>()
val optionsMenuTranslateY = MutableLiveData<Float>()
val audioRoutesMenuTranslateY = MutableLiveData<Float>()
val toggleNumpadEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
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()
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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 <T : ViewModel> create(modelClass: Class<T>): T {
return IncomingCallViewModel(call) as T
}
}
class IncomingCallViewModel(call: Call) : CallViewModel(call) {
val screenLocked = MutableLiveData<Boolean>()
val earlyMediaVideoEnabled = MutableLiveData<Boolean>()
val inviteWithVideo = MutableLiveData<Boolean>()
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)
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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()
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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()
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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()
}
}

View file

@ -119,7 +119,7 @@ class ChatBubbleActivity : GenericActivity() {
adapter.registerAdapterDataObserver(observer)
// Disable context menu on each message
adapter.disableContextMenu()
adapter.disableAdvancedContextMenuOptions()
adapter.openContentEvent.observe(
this

View file

@ -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<Event<String>>()
}
val callConferenceEvent: MutableLiveData<Event<Pair<String, String?>>> by lazy {
MutableLiveData<Event<Pair<String, String?>>>()
}
val scrollToChatMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
MutableLiveData<Event<ChatMessage>>()
}
@ -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

View file

@ -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<Boolean>()
val isGenericFile = MutableLiveData<Boolean>()
val isVoiceRecording = MutableLiveData<Boolean>()
val isConferenceSchedule = MutableLiveData<Boolean>()
val fileName = MutableLiveData<String>()
val filePath = MutableLiveData<String>()
@ -71,6 +77,15 @@ class ChatMessageContentData(
val voiceRecordPlayingPosition = MutableLiveData<Int>()
val isVoiceRecordPlaying = MutableLiveData<Boolean>()
val conferenceSubject = MutableLiveData<String>()
val conferenceDescription = MutableLiveData<String>()
val conferenceParticipantCount = MutableLiveData<String>()
val conferenceDate = MutableLiveData<String>()
val conferenceTime = MutableLiveData<String>()
val conferenceDuration = MutableLiveData<String>()
var conferenceAddress = MutableLiveData<String>()
val showDuration = MutableLiveData<Boolean>()
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?)
}

View file

@ -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}")
}
}

View file

@ -44,6 +44,6 @@ class DevicesListChildData(private val device: ParticipantDevice) {
}
fun onClick() {
coreContext.startCall(device.address, true)
coreContext.startCall(device.address, forceZRTP = true)
}
}

View file

@ -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)
}
}

View file

@ -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<ChatRoomCreationFragmentBinding>() {
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<ChatRoomCreationFragmentBinding>
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<ChatRoomCreationFragmentBinding>
viewModel.isEncrypted.observe(
viewLifecycleOwner
) {
adapter.updateSecurity(it)
adapter.setLimeCapabilityRequired(it)
}
viewModel.sipContactsSelected.observe(
@ -152,7 +152,7 @@ class ChatRoomCreationFragment : SecureFragment<ChatRoomCreationFragmentBinding>
navigateToGroupInfo()
}
viewModel.onErrorEvent.observe(
viewModel.onMessageToNotifyEvent.observe(
viewLifecycleOwner
) {
it.consume { messageResourceId ->
@ -185,8 +185,8 @@ class ChatRoomCreationFragment : SecureFragment<ChatRoomCreationFragmentBinding>
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")
}

View file

@ -161,7 +161,6 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
val chatRoom = sharedViewModel.selectedChatRoom.value
if (chatRoom == null) {
Log.e("[Chat Room] Chat room is null, aborting!")
// (activity as MainActivity).showSnackBar(R.string.error)
goBack()
return
}
@ -406,11 +405,29 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
viewLifecycleOwner
) {
it.consume { content ->
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<ChatRoomDetailFragmentBinding, Cha
}
}
adapter.callConferenceEvent.observe(
viewLifecycleOwner
) {
it.consume { pair ->
navigateToConferenceWaitingRoom(pair.first, pair.second)
}
}
adapter.scrollToChatMessageEvent.observe(
viewLifecycleOwner
) {
@ -542,7 +567,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
if (voiceRecordingDuration < 1000) {
Log.w("[Chat Room] Voice recording button has been held for less than a second, considering miss click")
chatSendingViewModel.cancelVoiceRecording()
(requireActivity() as MainActivity).showSnackBar(R.string.chat_message_voice_recording_hold_to_record)
(activity as MainActivity).showSnackBar(R.string.chat_message_voice_recording_hold_to_record)
} else {
Log.i("[Chat Room] Voice recording button has been released, stop recording")
chatSendingViewModel.stopVoiceRecording()
@ -731,7 +756,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
val address = viewModel.onlyParticipantOnlyDeviceAddress
if (viewModel.oneParticipantOneDevice) {
if (address != null) {
coreContext.startCall(address, true)
coreContext.startCall(address, forceZRTP = true)
}
} else {
navigateToDevices()
@ -747,7 +772,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
val address = viewModel.onlyParticipantOnlyDeviceAddress
if (viewModel.oneParticipantOneDevice) {
if (address != null) {
coreContext.startCall(address, true)
coreContext.startCall(address, forceZRTP = true)
}
} else {
navigateToDevices()

View file

@ -49,7 +49,6 @@ class DevicesFragment : SecureFragment<ChatRoomDevicesFragmentBinding>() {
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
}

View file

@ -52,7 +52,6 @@ class EphemeralFragment : SecureFragment<ChatRoomEphemeralFragmentBinding>() {
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
}

View file

@ -175,7 +175,7 @@ class GroupInfoFragment : SecureFragment<ChatRoomGroupInfoFragmentBinding>() {
dialog.show()
}
viewModel.onErrorEvent.observe(
viewModel.onMessageToNotifyEvent.observe(
viewLifecycleOwner
) {
it.consume { messageResourceId ->

View file

@ -56,7 +56,6 @@ class ImdnFragment : SecureFragment<ChatRoomImdnFragmentBinding>() {
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
}

View file

@ -312,8 +312,6 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
) {
if (it.isNotEmpty()) {
Log.i("[Chat] Found text to share")
// val activity = requireActivity() as MainActivity
// activity.showSnackBar(R.string.chat_room_toast_choose_for_sharing)
listViewModel.textSharingPending.value = true
clearDisplayedChatRoom()
} else {
@ -327,8 +325,6 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
) {
if (it.isNotEmpty()) {
Log.i("[Chat] Found ${it.size} files to share")
// val activity = requireActivity() as MainActivity
// activity.showSnackBar(R.string.chat_room_toast_choose_for_sharing)
listViewModel.fileSharingPending.value = true
clearDisplayedChatRoom()
} else {
@ -347,7 +343,7 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
}
}
listViewModel.onErrorEvent.observe(
listViewModel.onMessageToNotifyEvent.observe(
viewLifecycleOwner
) {
it.consume { messageResourceId ->

View file

@ -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<Event<ChatRoom>> by lazy {
MutableLiveData<Event<ChatRoom>>()
}
val createGroupChat = MutableLiveData<Boolean>()
val sipContactsSelected = MutableLiveData<Boolean>()
val isEncrypted = MutableLiveData<Boolean>()
val contactsList = MutableLiveData<ArrayList<SearchResult>>()
val waitForChatRoomCreation = MutableLiveData<Boolean>()
val selectedAddresses = MutableLiveData<ArrayList<Address>>()
val filter = MutableLiveData<String>()
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<SearchResult>()
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<Address>()
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
}

View file

@ -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<String>()
val lastMessageText = MutableLiveData<String>()
val lastMessageText = MutableLiveData<SpannableStringBuilder>()
val callInProgress = MutableLiveData<Boolean>()
@ -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() {

View file

@ -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<ArrayList<ChatRoomViewModel>>()
val contactsUpdatedEvent: MutableLiveData<Event<Boolean>> 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)
}
}

View file

@ -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<Event<ChatRoom>>()
val updatedChatRoomEvent = MutableLiveData<Event<ChatRoom>>()
@ -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)
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<ScheduledConferenceData, RecyclerView.ViewHolder>(ConferenceInfoDiffCallback()),
HeaderAdapter {
val copyAddressToClipboardEvent: MutableLiveData<Event<Address>> by lazy {
MutableLiveData<Event<Address>>()
}
val joinConferenceEvent: MutableLiveData<Event<Pair<String, String?>>> by lazy {
MutableLiveData<Event<Pair<String, String?>>>()
}
val deleteConferenceInfoEvent: MutableLiveData<Event<ScheduledConferenceData>> by lazy {
MutableLiveData<Event<ScheduledConferenceData>>()
}
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<ScheduledConferenceData>() {
override fun areItemsTheSame(
oldItem: ScheduledConferenceData,
newItem: ScheduledConferenceData
): Boolean {
return oldItem.conferenceInfo == newItem.conferenceInfo
}
override fun areContentsTheSame(
oldItem: ScheduledConferenceData,
newItem: ScheduledConferenceData
): Boolean {
return false
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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)
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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 SharedCallViewModel : ViewModel() {
val toggleDrawerEvent = MutableLiveData<Event<Boolean>>()
val resetHiddenInterfaceTimerInVideoCallEvent = MutableLiveData<Event<Boolean>>()
class Duration(val value: Int, val display: String) : Comparable<Duration> {
override fun toString(): String {
return display
}
override fun compareTo(other: Duration): Int {
return value.compareTo(other.value)
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Boolean>()
val address = MutableLiveData<String>()
val subject = MutableLiveData<String>()
val description = MutableLiveData<String>()
val time = MutableLiveData<String>()
val date = MutableLiveData<String>()
val duration = MutableLiveData<String>()
val organizer = MutableLiveData<String>()
val participantsShort = MutableLiveData<String>()
val participantsExpanded = MutableLiveData<String>()
val showDuration = MutableLiveData<Boolean>()
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
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.main.conference.data
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.abs
class TimeZoneData(timeZone: TimeZone) : Comparable<TimeZoneData> {
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)
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<ConferenceSchedulingFragmentBinding>() {
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")
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<ConferenceSchedulingParticipantsListFragmentBinding>() {
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<out String>,
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")
}
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<ConferenceSchedulingSummaryFragmentBinding>() {
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()
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<ConferenceWaitingRoomFragmentBinding>() {
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<String>()
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<String>(permissionsRequiredList.size)
permissionsRequiredList.toArray(permissionsRequired)
requestPermissions(permissionsRequired, 0)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
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)
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<ConferencesScheduledFragmentBinding>() {
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()
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String>()
val description = MutableLiveData<String>()
val scheduleForLater = MutableLiveData<Boolean>()
val formattedDate = MutableLiveData<String>()
val formattedTime = MutableLiveData<String>()
val isEncrypted = MutableLiveData<Boolean>()
val sendInviteViaChat = MutableLiveData<Boolean>()
val sendInviteViaEmail = MutableLiveData<Boolean>()
val participantsData = MutableLiveData<List<ConferenceSchedulingParticipantData>>()
val address = MutableLiveData<Address>()
val conferenceCreationInProgress = MutableLiveData<Boolean>()
val conferenceCreationCompletedEvent: MutableLiveData<Event<Pair<String, String?>>> by lazy {
MutableLiveData<Event<Pair<String, String?>>>()
}
val continueEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
var timeZone = MutableLiveData<TimeZoneData>()
val timeZones: List<TimeZoneData> = computeTimeZonesList()
var duration = MutableLiveData<Duration>()
val durationList: List<Duration> = 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<out Address>?
) {
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<ConferenceSchedulingParticipantData>()
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<Address>(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<TimeZoneData> {
return TimeZone.getAvailableIDs().map { id -> TimeZoneData(TimeZone.getTimeZone(id)) }.toList().sorted()
}
private fun computeDurationList(): List<Duration> {
// 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)
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String>()
val isMicrophoneMuted = MutableLiveData<Boolean>()
val audioRoutesEnabled = MutableLiveData<Boolean>()
val audioRoutesSelected = MutableLiveData<Boolean>()
val isSpeakerSelected = MutableLiveData<Boolean>()
val isBluetoothHeadsetSelected = MutableLiveData<Boolean>()
val isVideoAvailable = MutableLiveData<Boolean>()
val isVideoEnabled = MutableLiveData<Boolean>()
val isSwitchCameraAvailable = MutableLiveData<Boolean>()
val joinInProgress = MutableLiveData<Boolean>()
val askPermissionEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
val cancelConferenceJoiningEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val joinConferenceEvent: MutableLiveData<Event<CallParams>> by lazy {
MutableLiveData<Event<CallParams>>()
}
val leaveWaitingRoomEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
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
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<ArrayList<ScheduledConferenceData>>()
private val listener = object : CoreListenerStub() {
override fun onConferenceInfoReceived(core: Core, conferenceInfo: ConferenceInfo) {
Log.i("[Scheduled Conferences] New conference info received")
val conferencesList = arrayListOf<ScheduledConferenceData>()
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<ScheduledConferenceData>()
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<ScheduledConferenceData>()
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")
}
}

View file

@ -138,7 +138,7 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), 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()
}
}

View file

@ -71,7 +71,6 @@ class DetailContactFragment : GenericFragment<ContactDetailFragmentBinding>() {
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<ContactDetailFragmentBinding>() {
confirmContactRemoval()
}
viewModel.onErrorEvent.observe(
viewModel.onMessageToNotifyEvent.observe(
viewLifecycleOwner
) {
it.consume { messageResourceId ->

View file

@ -138,7 +138,6 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
}
}
binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
/* End of shared view model & sliding pane related */
_adapter = ContactsListAdapter(listSelectionViewModel, viewLifecycleOwner)
@ -282,8 +281,7 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
} else if (sipUri != null) {
Log.i("[Contacts] Found sipUri parameter in arguments: $sipUri")
sipUriToAdd = sipUri
val activity = requireActivity() as MainActivity
activity.showSnackBar(R.string.contact_choose_existing_or_new_to_add_number)
(activity as MainActivity).showSnackBar(R.string.contact_choose_existing_or_new_to_add_number)
editOnClick = true
} else if (addressString != null) {
val address = Factory.instance().createAddress(addressString)

View file

@ -29,7 +29,7 @@ import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.activities.main.contact.data.ContactNumberOrAddressClickListener
import org.linphone.activities.main.contact.data.ContactNumberOrAddressData
import org.linphone.activities.main.viewmodels.ErrorReportingViewModel
import org.linphone.activities.main.viewmodels.MessageNotifierViewModel
import org.linphone.contact.Contact
import org.linphone.contact.ContactDataInterface
import org.linphone.contact.ContactsUpdatedListenerStub
@ -48,7 +48,7 @@ class ContactViewModelFactory(private val contact: Contact) :
}
}
class ContactViewModel(val contactInternal: Contact) : ErrorReportingViewModel(), ContactDataInterface {
class ContactViewModel(val contactInternal: Contact) : MessageNotifierViewModel(), ContactDataInterface {
override val contact: MutableLiveData<Contact> = MutableLiveData<Contact>()
override val displayName: MutableLiveData<String> = MutableLiveData<String>()
override val securityLevel: MutableLiveData<ChatRoomSecurityLevel> = MutableLiveData<ChatRoomSecurityLevel>()
@ -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)
}
}

View file

@ -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<DialerFragmentBinding>() {
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<DialerFragmentBinding>() {
}
}
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

View file

@ -47,6 +47,8 @@ class DialerViewModel : LogsUploadViewModel() {
val autoInitiateVideoCalls = MutableLiveData<Boolean>()
val scheduleConferenceAvailable = MutableLiveData<Boolean>()
val updateAvailableEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
@ -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 {

View file

@ -48,7 +48,6 @@ class AudioViewerFragment : GenericViewerFragment<FileAudioViewerFragmentBinding
val content = sharedViewModel.contentToOpen.value
if (content == null) {
Log.e("[Audio Viewer] Content is null, aborting!")
// (activity as MainActivity).showSnackBar(R.string.error)
findNavController().navigateUp()
return
}

View file

@ -48,7 +48,6 @@ abstract class GenericViewerFragment<T : ViewDataBinding> : SecureFragment<T>()
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
}

View file

@ -44,7 +44,6 @@ class ImageViewerFragment : GenericViewerFragment<FileImageViewerFragmentBinding
val content = sharedViewModel.contentToOpen.value
if (content == null) {
Log.e("[Image Viewer] Content is null, aborting!")
// (activity as MainActivity).showSnackBar(R.string.error)
findNavController().navigateUp()
return
}

View file

@ -44,7 +44,6 @@ class PdfViewerFragment : GenericViewerFragment<FilePdfViewerFragmentBinding>()
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
}

View file

@ -42,7 +42,6 @@ class TextViewerFragment : GenericViewerFragment<FileTextViewerFragmentBinding>(
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
}

View file

@ -46,7 +46,6 @@ class VideoViewerFragment : GenericViewerFragment<FileVideoViewerFragmentBinding
val content = sharedViewModel.contentToOpen.value
if (content == null) {
Log.e("[Video Viewer] Content is null, aborting!")
// (activity as MainActivity).showSnackBar(R.string.error)
findNavController().navigateUp()
return
}

View file

@ -65,7 +65,7 @@ class CallLogsListAdapter(
) : RecyclerView.ViewHolder(binding.root) {
fun bind(callLogGroup: GroupedCallLogData) {
with(binding) {
val callLogViewModel = callLogGroup.lastCallLogData
val callLogViewModel = callLogGroup.lastCallLogViewModel
viewModel = callLogViewModel
lifecycleOwner = viewLifecycleOwner

View file

@ -1,77 +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 <http://www.gnu.org/licenses/>.
*/
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)
}
}

View file

@ -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()
}
}

View file

@ -57,7 +57,6 @@ class DetailCallLogFragment : GenericFragment<HistoryDetailFragmentBinding>() {
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<HistoryDetailFragmentBinding>() {
useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false
viewModel.addRelatedCallLogs(callLogGroup.callLogs)
viewModel.relatedCallLogs.value = callLogGroup.callLogs
binding.setBackClickListener {
goBack()
@ -134,7 +133,7 @@ class DetailCallLogFragment : GenericFragment<HistoryDetailFragmentBinding>() {
}
}
viewModel.onErrorEvent.observe(
viewModel.onMessageToNotifyEvent.observe(
viewLifecycleOwner
) {
it.consume { messageResourceId ->

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<HistoryConfDetailFragmentBinding>() {
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()
}
}
}

View file

@ -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<HistoryMasterFragmentBinding, Call
}
}
binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
/* End of shared view model & sliding pane related */
_adapter = CallLogsListAdapter(listSelectionViewModel, viewLifecycleOwner)
@ -191,31 +192,11 @@ class MasterCallLogsFragment : MasterFragment<HistoryMasterFragmentBinding, Call
val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter)
binding.callLogsList.addItemDecoration(headerItemDecoration)
listViewModel.callLogs.observe(
listViewModel.displayedCallLogs.observe(
viewLifecycleOwner
) { callLogs ->
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)
}
}
listViewModel.contactsUpdatedEvent.observe(
viewLifecycleOwner
@ -230,24 +211,29 @@ class MasterCallLogsFragment : MasterFragment<HistoryMasterFragmentBinding, Call
) {
it.consume { callLog ->
sharedViewModel.selectedCallLogGroup.value = callLog
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<HistoryMasterFragmentBinding, Call
}
}
binding.setAllCallLogsToggleClickListener {
listViewModel.missedCallLogsSelected.value = false
}
binding.setMissedCallLogsToggleClickListener {
listViewModel.missedCallLogsSelected.value = true
}
coreContext.core.resetMissedCallsCount()
coreContext.notificationsManager.dismissMissedCallNotification()
}

View file

@ -22,16 +22,20 @@ package org.linphone.activities.main.history.viewmodels
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.activities.main.history.data.CallLogData
import org.linphone.activities.main.conference.data.ConferenceSchedulingParticipantData
import org.linphone.contact.GenericContactViewModel
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
import org.linphone.utils.TimestampUtils
class CallLogViewModelFactory(private val callLog: CallLog) :
ViewModelProvider.NewInstanceFactory() {
@ -47,6 +51,53 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
LinphoneUtils.getDisplayableAddress(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)
}
val startCallEvent: MutableLiveData<Event<CallLog>> by lazy {
MutableLiveData<Event<CallLog>>()
}
@ -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<ArrayList<CallLogData>>()
val relatedCallLogs = MutableLiveData<ArrayList<CallLog>>()
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<CallLog>()
list.add(callLog)
list.addAll(relatedCallLogs.value.orEmpty())
relatedCallLogs.value = list
}
}
}
val isConferenceCallLog = callLog.wasConference()
val conferenceSubject = callLog.conferenceInfo?.subject
val conferenceParticipantsData = MutableLiveData<ArrayList<ConferenceSchedulingParticipantData>>()
val conferenceTime = MutableLiveData<String>()
val conferenceDate = MutableLiveData<String>()
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<ConferenceSchedulingParticipantData>()
for (participant in conferenceInfo.participants) {
list.add(ConferenceSchedulingParticipantData(participant, false))
}
conferenceParticipantsData.value = list
}
}
override fun onCleared() {
coreContext.core.removeListener(listener)
destroy()
super.onCleared()
}
fun destroy() {
relatedCallLogs.value.orEmpty().forEach(CallLogData::destroy)
coreContext.core.removeListener(listener)
conferenceParticipantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy)
}
fun startCall() {
@ -119,19 +197,7 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
} else {
waitForChatRoomCreation.value = false
Log.e("[History Detail] Couldn't create chat room with address ${callLog.remoteAddress}")
onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack)
onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
}
}
fun addRelatedCallLogs(logs: ArrayList<CallLog>) {
val callsHistory = ArrayList<CallLogData>()
// We assume new logs are more recent than the ones we already have, so we add them first
for (log in logs) {
callsHistory.add(CallLogData(log))
}
callsHistory.addAll(relatedCallLogs.value.orEmpty())
relatedCallLogs.value = callsHistory
}
}

View file

@ -31,15 +31,20 @@ import org.linphone.utils.LinphoneUtils
import org.linphone.utils.TimestampUtils
class CallLogsListViewModel : ViewModel() {
val callLogs = MutableLiveData<ArrayList<GroupedCallLogData>>()
val missedCallLogs = MutableLiveData<ArrayList<GroupedCallLogData>>()
val displayedCallLogs = MutableLiveData<List<GroupedCallLogData>>()
val missedCallLogsSelected = MutableLiveData<Boolean>()
val filter = MutableLiveData<CallLogsFilter>()
val showConferencesFilter = MutableLiveData<Boolean>()
val contactsUpdatedEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
private val callLogs = MutableLiveData<ArrayList<GroupedCallLogData>>()
private val missedCallLogs = MutableLiveData<ArrayList<GroupedCallLogData>>()
private val conferenceCallLogs = MutableLiveData<ArrayList<GroupedCallLogData>>()
private val listener: CoreListenerStub = object : CoreListenerStub() {
override fun onCallStateChanged(
core: Core,
@ -59,9 +64,11 @@ class CallLogsListViewModel : ViewModel() {
}
init {
missedCallLogsSelected.value = false
filter.value = CallLogsFilter.ALL
updateCallLogs()
showConferencesFilter.value = LinphoneUtils.isRemoteConferencingAvailable()
coreContext.core.addListener(listener)
coreContext.contactsManager.addListener(contactsUpdatedListener)
}
@ -69,6 +76,8 @@ class CallLogsListViewModel : ViewModel() {
override fun onCleared() {
callLogs.value.orEmpty().forEach(GroupedCallLogData::destroy)
missedCallLogs.value.orEmpty().forEach(GroupedCallLogData::destroy)
conferenceCallLogs.value.orEmpty().forEach(GroupedCallLogData::destroy)
displayedCallLogs.value.orEmpty().forEach(GroupedCallLogData::destroy)
coreContext.contactsManager.removeListener(contactsUpdatedListener)
coreContext.core.removeListener(listener)
@ -76,6 +85,21 @@ class CallLogsListViewModel : ViewModel() {
super.onCleared()
}
fun showAllCallLogs() {
filter.value = CallLogsFilter.ALL
applyFilter()
}
fun showOnlyMissedCallLogs() {
filter.value = CallLogsFilter.MISSED
applyFilter()
}
fun showOnlyConferenceCallLogs() {
filter.value = CallLogsFilter.CONFERENCE
applyFilter()
}
fun deleteCallLogGroup(callLog: GroupedCallLogData?) {
if (callLog != null) {
for (log in callLog.callLogs) {
@ -96,22 +120,22 @@ class CallLogsListViewModel : ViewModel() {
updateCallLogs()
}
private fun updateCallLogs() {
callLogs.value.orEmpty().forEach(GroupedCallLogData::destroy)
missedCallLogs.value.orEmpty().forEach(GroupedCallLogData::destroy)
val list = arrayListOf<GroupedCallLogData>()
val missedList = arrayListOf<GroupedCallLogData>()
private fun computeCallLogs(callLogs: Array<CallLog>, missed: Boolean, conference: Boolean): ArrayList<GroupedCallLogData> {
var previousCallLogGroup: GroupedCallLogData? = null
var previousMissedCallLogGroup: GroupedCallLogData? = null
for (callLog in coreContext.core.callLogs) {
val list = arrayListOf<GroupedCallLogData>()
for (callLog in callLogs) {
if ((!missed && !conference) || (missed && LinphoneUtils.isCallLogMissed(callLog)) || (conference && callLog.wasConference())) {
if (previousCallLogGroup == null) {
previousCallLogGroup = GroupedCallLogData(callLog)
} else if (previousCallLogGroup.lastCallLog.localAddress.weakEqual(callLog.localAddress) &&
previousCallLogGroup.lastCallLog.remoteAddress.weakEqual(callLog.remoteAddress)
previousCallLogGroup.lastCallLog.remoteAddress.equal(callLog.remoteAddress)
) {
if (TimestampUtils.isSameDay(
previousCallLogGroup.lastCallLog.startDate,
callLog.startDate
)
) {
if (TimestampUtils.isSameDay(previousCallLogGroup.lastCallLog.startDate, callLog.startDate)) {
previousCallLogGroup.callLogs.add(callLog)
previousCallLogGroup.lastCallLog = callLog
} else {
@ -122,35 +146,43 @@ class CallLogsListViewModel : ViewModel() {
list.add(previousCallLogGroup)
previousCallLogGroup = GroupedCallLogData(callLog)
}
if (LinphoneUtils.isCallLogMissed(callLog)) {
if (previousMissedCallLogGroup == null) {
previousMissedCallLogGroup = GroupedCallLogData(callLog)
} else if (previousMissedCallLogGroup.lastCallLog.localAddress.weakEqual(callLog.localAddress) &&
previousMissedCallLogGroup.lastCallLog.remoteAddress.weakEqual(callLog.remoteAddress)
) {
if (TimestampUtils.isSameDay(previousMissedCallLogGroup.lastCallLog.startDate, callLog.startDate)) {
previousMissedCallLogGroup.callLogs.add(callLog)
previousMissedCallLogGroup.lastCallLog = callLog
} else {
missedList.add(previousMissedCallLogGroup)
previousMissedCallLogGroup = GroupedCallLogData(callLog)
}
} else {
missedList.add(previousMissedCallLogGroup)
previousMissedCallLogGroup = GroupedCallLogData(callLog)
}
}
}
if (previousCallLogGroup != null && !list.contains(previousCallLogGroup)) {
list.add(previousCallLogGroup)
}
if (previousMissedCallLogGroup != null && !missedList.contains(previousMissedCallLogGroup)) {
missedList.add(previousMissedCallLogGroup)
return list
}
callLogs.value = list
missedCallLogs.value = missedList
private fun updateCallLogs() {
callLogs.value.orEmpty().forEach(GroupedCallLogData::destroy)
missedCallLogs.value.orEmpty().forEach(GroupedCallLogData::destroy)
conferenceCallLogs.value.orEmpty().forEach(GroupedCallLogData::destroy)
val allCallLogs = coreContext.core.callLogs
callLogs.value = computeCallLogs(allCallLogs, false, false)
missedCallLogs.value = computeCallLogs(allCallLogs, true, false)
conferenceCallLogs.value = computeCallLogs(allCallLogs, false, true)
applyFilter()
}
private fun applyFilter() {
displayedCallLogs.value.orEmpty().forEach(GroupedCallLogData::destroy)
val displayedList = arrayListOf<GroupedCallLogData>()
when (filter.value) {
CallLogsFilter.MISSED -> displayedList.addAll(missedCallLogs.value.orEmpty())
CallLogsFilter.CONFERENCE -> displayedList.addAll(conferenceCallLogs.value.orEmpty())
else -> displayedList.addAll(callLogs.value.orEmpty())
}
displayedCallLogs.value = displayedList
}
}
enum class CallLogsFilter {
ALL,
MISSED,
CONFERENCE
}

View file

@ -46,7 +46,6 @@ class AccountSettingsFragment : GenericSettingFragment<SettingsAccountFragmentBi
val identity = arguments?.getString("Identity")
if (identity == null) {
Log.e("[Account Settings] Identity is null, aborting!")
// (activity as MainActivity).showSnackBar(R.string.error)
goBack()
return
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2010-2020 Belledonne Communications SARL.
* Copyright (c) 2010-2022 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
@ -17,33 +17,39 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.call.fragments
package org.linphone.activities.main.settings.fragments
import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModelProvider
import org.linphone.R
import org.linphone.activities.GenericFragment
import org.linphone.activities.call.viewmodels.StatisticsListViewModel
import org.linphone.databinding.CallStatisticsFragmentBinding
import org.linphone.activities.main.settings.viewmodels.ConferencesSettingsViewModel
import org.linphone.activities.navigateToEmptySetting
import org.linphone.databinding.SettingsConferencesFragmentBinding
import org.linphone.utils.Event
class StatisticsFragment : GenericFragment<CallStatisticsFragmentBinding>() {
private lateinit var viewModel: StatisticsListViewModel
class ConferencesSettingsFragment : GenericSettingFragment<SettingsConferencesFragmentBinding>() {
private lateinit var viewModel: ConferencesSettingsViewModel
override fun getLayoutId(): Int = R.layout.call_statistics_fragment
override fun getLayoutId(): Int = R.layout.settings_conferences_fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
useMaterialSharedAxisXForwardAnimation = false
binding.sharedMainViewModel = sharedViewModel
viewModel = ViewModelProvider(this)[StatisticsListViewModel::class.java]
viewModel = ViewModelProvider(this)[ConferencesSettingsViewModel::class.java]
binding.viewModel = viewModel
binding.setBackClickListener { goBack() }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onBackPressedCallback.isEnabled = false
override fun goBack() {
if (sharedViewModel.isSlidingPaneSlideable.value == true) {
sharedViewModel.closeSlidingPaneEvent.value = Event(true)
} else {
navigateToEmptySetting()
}
}
}

View file

@ -177,5 +177,11 @@ class SettingsFragment : SecureFragment<SettingsFragmentBinding>() {
navigateToAdvancedSettings(binding.slidingPane)
}
}
viewModel.conferencesSettingsListener = object : SettingListenerStub() {
override fun onClicked() {
navigateToConferencesSettings(binding.slidingPane)
}
}
}
}

View file

@ -390,6 +390,27 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
}
val linkPhoneNumberEvent = MutableLiveData<Event<Boolean>>()
val conferenceFactoryUriListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) {
val params = account.params.clone()
Log.i("[Account Settings] Forcing conference factory on proxy config ${params.identityAddress?.asString()} to value: $newValue")
params.conferenceFactoryUri = newValue
account.params = params
}
}
val conferenceFactoryUri = MutableLiveData<String>()
val audioVideoConferenceFactoryUriListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) {
val params = account.params.clone()
val uri = coreContext.core.interpretUrl(newValue)
Log.i("[Account Settings] Forcing audio/video conference factory on proxy config ${params.identityAddress?.asString()} to value: $newValue")
params.audioVideoConferenceFactoryAddress = uri
account.params = params
}
}
val audioVideoConferenceFactoryUri = MutableLiveData<String>()
init {
update()
account.addListener(listener)
@ -444,6 +465,9 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
prefix.value = params.internationalPrefix
dialPrefix.value = params.useInternationalPrefixForCallsAndChats
escapePlus.value = params.isDialEscapePlusEnabled
conferenceFactoryUri.value = params.conferenceFactoryUri
audioVideoConferenceFactoryUri.value = params.audioVideoConferenceFactoryAddress?.asStringUriOnly()
}
private fun initTransportList() {

View file

@ -105,13 +105,6 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
}
val api26OrHigher = MutableLiveData<Boolean>()
val fullScreenListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
prefs.fullScreenCallUI = newValue
}
}
val fullScreen = MutableLiveData<Boolean>()
val overlayListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
prefs.showCallOverlay = newValue
@ -151,6 +144,13 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
}
val autoStartCallRecording = MutableLiveData<Boolean>()
val remoteCallRecordingListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
core.isRecordAwareEnabled = newValue
}
}
val remoteCallRecording = MutableLiveData<Boolean>()
val autoStartListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
prefs.callRightAway = newValue
@ -242,12 +242,12 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
useTelecomManager.value = prefs.useTelecomManager
api26OrHigher.value = Version.sdkAboveOrEqual(Version.API26_O_80)
fullScreen.value = prefs.fullScreenCallUI
overlay.value = prefs.showCallOverlay
systemWideOverlay.value = prefs.systemWideCallOverlay
sipInfoDtmf.value = core.useInfoForDtmf
rfc2833Dtmf.value = core.useRfc2833ForDtmf
autoStartCallRecording.value = prefs.automaticallyStartCallRecording
remoteCallRecording.value = core.isRecordAwareEnabled
autoStart.value = prefs.callRightAway
autoAnswer.value = prefs.autoAnswerEnabled
autoAnswerDelay.value = prefs.autoAnswerDelay

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2010-2022 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.main.settings.viewmodels
import androidx.lifecycle.MutableLiveData
import org.linphone.R
import org.linphone.activities.main.settings.SettingListenerStub
import org.linphone.core.ConferenceLayout
class ConferencesSettingsViewModel : GenericSettingsViewModel() {
val layoutListener = object : SettingListenerStub() {
override fun onListValueChanged(position: Int) {
core.defaultConferenceLayout = ConferenceLayout.fromInt(layoutValues[position])
layoutIndex.value = position
}
}
val layoutIndex = MutableLiveData<Int>()
val layoutLabels = MutableLiveData<ArrayList<String>>()
private val layoutValues = arrayListOf<Int>()
init {
initLayoutsList()
}
private fun initLayoutsList() {
val labels = arrayListOf<String>()
labels.add(prefs.getString(R.string.conference_display_mode_active_speaker))
layoutValues.add(ConferenceLayout.ActiveSpeaker.toInt())
labels.add(prefs.getString(R.string.conference_display_mode_mosaic))
layoutValues.add(ConferenceLayout.Grid.toInt())
layoutLabels.value = labels
layoutIndex.value = layoutValues.indexOf(core.defaultConferenceLayout.toInt())
}
}

View file

@ -37,6 +37,7 @@ class SettingsViewModel : ViewModel() {
val showNetworkSettings: Boolean = corePreferences.showNetworkSettings
val showContactsSettings: Boolean = corePreferences.showContactsSettings
val showAdvancedSettings: Boolean = corePreferences.showAdvancedSettings
val showConferencesSettings: Boolean = corePreferences.showConferencesSettings
val accounts = MutableLiveData<ArrayList<AccountSettingsViewModel>>()
@ -64,6 +65,8 @@ class SettingsViewModel : ViewModel() {
lateinit var advancedSettingsListener: SettingListenerStub
lateinit var conferencesSettingsListener: SettingListenerStub
val primaryAccountDisplayNameListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) {
val address = coreContext.core.createPrimaryContactParsed()

View file

@ -32,7 +32,7 @@ import java.io.File
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.activities.GenericFragment
import org.linphone.activities.*
import org.linphone.activities.assistant.AssistantActivity
import org.linphone.activities.main.settings.SettingListenerStub
import org.linphone.activities.main.sidemenu.viewmodels.SideMenuViewModel
@ -108,6 +108,11 @@ class SideMenuFragment : GenericFragment<SideMenuFragmentBinding>() {
navigateToAbout()
}
binding.setConferencesClickListener {
sharedViewModel.toggleDrawerEvent.value = Event(true)
navigateToScheduledConferences()
}
binding.setQuitClickListener {
Log.i("[Side Menu] Quitting app")
requireActivity().finishAndRemoveTask()

View file

@ -26,12 +26,15 @@ import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.activities.main.settings.SettingListenerStub
import org.linphone.activities.main.settings.viewmodels.AccountSettingsViewModel
import org.linphone.core.*
import org.linphone.utils.LinphoneUtils
class SideMenuViewModel : ViewModel() {
val showAccounts: Boolean = corePreferences.showAccountsInSideMenu
val showAssistant: Boolean = corePreferences.showAssistantInSideMenu
val showSettings: Boolean = corePreferences.showSettingsInSideMenu
val showRecordings: Boolean = corePreferences.showRecordingsInSideMenu
val showScheduledConferences: Boolean = corePreferences.showScheduledConferencesInSideMenu &&
LinphoneUtils.isRemoteConferencingAvailable()
val showAbout: Boolean = corePreferences.showAboutInSideMenu
val showQuit: Boolean = corePreferences.showQuitInSideMenu

View file

@ -26,6 +26,7 @@ import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.core.Call
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.tools.Log
class CallOverlayViewModel : ViewModel() {
val displayCallOverlay = MutableLiveData<Boolean>()
@ -34,18 +35,20 @@ class CallOverlayViewModel : ViewModel() {
override fun onCallStateChanged(
core: Core,
call: Call,
state: Call.State,
state: Call.State?,
message: String
) {
if (state == Call.State.IncomingReceived || state == Call.State.OutgoingInit) {
if (core.callsNb == 1 && call.state == Call.State.Connected) {
Log.i("[Call Overlay] First call connected, creating it")
createCallOverlay()
} else if (state == Call.State.End || state == Call.State.Error || state == Call.State.Released) {
if (core.callsNb == 0) {
}
}
override fun onLastCallEnded(core: Core) {
Log.i("[Call Overlay] Last call ended, removing it")
removeCallOverlay()
}
}
}
}
init {
displayCallOverlay.value = corePreferences.showCallOverlay &&

View file

@ -21,13 +21,12 @@
package org.linphone.activities.main.viewmodels
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.utils.Event
open class LogsUploadViewModel : ViewModel() {
open class LogsUploadViewModel : MessageNotifierViewModel() {
val uploadInProgress = MutableLiveData<Boolean>()
val resetCompleteEvent: MutableLiveData<Event<Boolean>> by lazy {

View file

@ -23,7 +23,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.utils.Event
/* Helper for view models to notify user of an error through a Snackbar */
abstract class ErrorReportingViewModel : ViewModel() {
val onErrorEvent = MutableLiveData<Event<Int>>()
/* Helper for view models to notify user of a massage through a Snackbar */
abstract class MessageNotifierViewModel : ViewModel() {
val onMessageToNotifyEvent = MutableLiveData<Event<Int>>()
}

View file

@ -101,7 +101,6 @@ class TabsViewModel : ViewModel() {
}
override fun onCleared() {
if (corePreferences.enableAnimations) bounceAnimator.end()
coreContext.core.removeListener(listener)
super.onCleared()
}

View file

@ -0,0 +1,260 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.voip
import android.Manifest
import android.annotation.TargetApi
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Bundle
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.findNavController
import androidx.window.layout.FoldingFeature
import org.linphone.LinphoneApplication
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.activities.ProximitySensorActivity
import org.linphone.activities.main.MainActivity
import org.linphone.activities.navigateToActiveCall
import org.linphone.activities.navigateToIncomingCall
import org.linphone.activities.navigateToOutgoingCall
import org.linphone.activities.voip.viewmodels.CallsViewModel
import org.linphone.activities.voip.viewmodels.ControlsViewModel
import org.linphone.compatibility.Compatibility
import org.linphone.core.Call
import org.linphone.core.tools.Log
import org.linphone.databinding.VoipActivityBinding
import org.linphone.mediastream.Version
import org.linphone.utils.PermissionHelper
class CallActivity : ProximitySensorActivity() {
private lateinit var binding: VoipActivityBinding
private lateinit var controlsViewModel: ControlsViewModel
private lateinit var callsViewModel: CallsViewModel
private var foldingFeature: FoldingFeature? = null
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.voip_activity)
binding.lifecycleOwner = this
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
// This can't be done in onCreate(), has to be at least in onPostCreate() !
val navController = binding.navHostFragment.findNavController()
val navControllerStoreOwner = navController.getViewModelStoreOwner(R.id.call_nav_graph)
controlsViewModel = ViewModelProvider(navControllerStoreOwner)[ControlsViewModel::class.java]
binding.controlsViewModel = controlsViewModel
callsViewModel = ViewModelProvider(navControllerStoreOwner)[CallsViewModel::class.java]
callsViewModel.noMoreCallEvent.observe(
this
) {
it.consume {
finish()
}
}
controlsViewModel.askPermissionEvent.observe(
this
) {
it.consume { permission ->
Log.i("[Call] Asking for $permission permission")
requestPermissions(arrayOf(permission), 0)
}
}
controlsViewModel.fullScreenMode.observe(
this
) { hide ->
Compatibility.hideAndroidSystemUI(hide, window)
}
controlsViewModel.proximitySensorEnabled.observe(
this
) { enabled ->
enableProximitySensor(enabled)
}
controlsViewModel.isVideoEnabled.observe(
this
) { enabled ->
Compatibility.enableAutoEnterPiP(this, enabled)
}
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
checkPermissions()
}
}
override fun onUserLeaveHint() {
super.onUserLeaveHint()
if (coreContext.core.currentCall?.currentParams?.isVideoEnabled ?: false) {
Log.i("[Call] Entering PiP mode")
Compatibility.enterPipMode(this)
}
}
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration
) {
Log.i("[Call] Activity is in PiP mode? $isInPictureInPictureMode")
if (::controlsViewModel.isInitialized) {
// To hide UI except for TextureViews
controlsViewModel.pipMode.value = isInPictureInPictureMode
}
}
override fun onResume() {
super.onResume()
if (coreContext.core.callsNb == 0) {
Log.w("[Call] 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()
val currentCall = coreContext.core.currentCall
if (currentCall == null) {
Log.e("[Call] No current call found, assume active call")
navigateToActiveCall()
return
}
when (currentCall.state) {
Call.State.OutgoingInit, Call.State.OutgoingEarlyMedia, Call.State.OutgoingProgress, Call.State.OutgoingRinging -> {
navigateToOutgoingCall()
}
Call.State.IncomingReceived, Call.State.IncomingEarlyMedia -> {
val earlyMediaVideoEnabled = LinphoneApplication.corePreferences.acceptEarlyMedia &&
currentCall.state == Call.State.IncomingEarlyMedia &&
currentCall.currentParams.isVideoEnabled
navigateToIncomingCall(earlyMediaVideoEnabled)
}
else -> navigateToActiveCall()
}
}
}
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()
}
@TargetApi(Version.API23_MARSHMALLOW_60)
private fun checkPermissions() {
val permissionsRequiredList = arrayListOf<String>()
if (!PermissionHelper.get().hasRecordAudioPermission()) {
Log.i("[Call] Asking for RECORD_AUDIO permission")
permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO)
}
if (callsViewModel.currentCallData.value?.call?.currentParams?.isVideoEnabled == true &&
!PermissionHelper.get().hasCameraPermission()
) {
Log.i("[Call] Asking for CAMERA permission")
permissionsRequiredList.add(Manifest.permission.CAMERA)
}
if (permissionsRequiredList.isNotEmpty()) {
val permissionsRequired = arrayOfNulls<String>(permissionsRequiredList.size)
permissionsRequiredList.toArray(permissionsRequired)
requestPermissions(permissionsRequired, 0)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
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("[Call] RECORD_AUDIO permission has been granted")
controlsViewModel.updateMicState()
}
Manifest.permission.CAMERA -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
Log.i("[Call] CAMERA permission has been granted")
coreContext.core.reloadVideoDevices()
}
}
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
override fun onLayoutChanges(foldingFeature: FoldingFeature?) {
this.foldingFeature = foldingFeature
updateConstraintSetDependingOnFoldingState()
}
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)*/
}
}

View file

@ -0,0 +1,314 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.voip.data
import android.view.View
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import java.util.*
import kotlinx.coroutines.*
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.contact.GenericContactData
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
open class CallData(val call: Call) : GenericContactData(call.remoteAddress) {
interface CallContextMenuClickListener {
fun onShowContextMenu(anchor: View, callData: CallData)
}
val address = call.remoteAddress.asStringUriOnly()
val isPaused = MutableLiveData<Boolean>()
val isRemotelyPaused = MutableLiveData<Boolean>()
val canBePaused = MutableLiveData<Boolean>()
val isRecording = MutableLiveData<Boolean>()
val isRemotelyRecorded = MutableLiveData<Boolean>()
val isInRemoteConference = MutableLiveData<Boolean>()
val remoteConferenceSubject = MutableLiveData<String>()
val isActiveAndNotInConference = MediatorLiveData<Boolean>()
val isOutgoing = MutableLiveData<Boolean>()
val isIncoming = MutableLiveData<Boolean>()
var chatRoom: ChatRoom? = null
var contextMenuClickListener: CallContextMenuClickListener? = null
private var timer: Timer? = null
private val listener = object : CallListenerStub() {
override fun onStateChanged(call: Call, state: Call.State, message: String) {
if (call != this@CallData.call) return
Log.i("[Call] State changed: $state")
update()
if (call.state == Call.State.UpdatedByRemote) {
val remoteVideo = call.remoteParams?.isVideoEnabled ?: false
val localVideo = call.currentParams.isVideoEnabled
if (remoteVideo && !localVideo) {
// User has 30 secs to accept or decline call update
startVideoUpdateAcceptanceTimer()
}
} else if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) {
timer?.cancel()
} else if (state == Call.State.StreamsRunning) {
// Stop call update timer once user has accepted or declined call update
timer?.cancel()
}
}
override fun onRemoteRecording(call: Call, recording: Boolean) {
Log.i("[Call] Remote recording changed: $recording")
isRemotelyRecorded.value = recording
}
}
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
init {
call.addListener(listener)
isRemotelyRecorded.value = call.remoteParams?.isRecording
isActiveAndNotInConference.value = true
isActiveAndNotInConference.addSource(isPaused) {
updateActiveAndNotInConference()
}
isActiveAndNotInConference.addSource(isRemotelyPaused) {
updateActiveAndNotInConference()
}
isActiveAndNotInConference.addSource(isInRemoteConference) {
updateActiveAndNotInConference()
}
update()
// initChatRoom()
val conferenceInfo = coreContext.core.findConferenceInformationFromUri(call.remoteAddress)
if (conferenceInfo != null) {
Log.i("[Call] Found matching conference info with subject: ${conferenceInfo.subject}")
remoteConferenceSubject.value = conferenceInfo.subject
}
}
override fun destroy() {
call.removeListener(listener)
timer?.cancel()
scope.cancel()
super.destroy()
}
fun togglePause() {
if (isCallPaused()) {
resume()
} else {
pause()
}
}
fun pause() {
call.pause()
}
fun resume() {
call.resume()
}
fun accept() {
call.accept()
}
fun terminate() {
call.terminate()
}
fun toggleRecording() {
if (call.isRecording) {
call.stopRecording()
} else {
call.startRecording()
}
isRecording.value = call.isRecording
}
fun showContextMenu(anchor: View) {
contextMenuClickListener?.onShowContextMenu(anchor, this)
}
private fun initChatRoom() {
val core = coreContext.core
val localSipUri = core.defaultAccount?.params?.identityAddress?.asStringUriOnly()
val remoteSipUri = call.remoteAddress.asStringUriOnly()
val conference = call.conference
if (localSipUri != null) {
val localAddress = Factory.instance().createAddress(localSipUri)
val remoteSipAddress = Factory.instance().createAddress(remoteSipUri)
chatRoom = core.searchChatRoom(null, localAddress, remoteSipAddress, arrayOfNulls(0))
if (chatRoom == null) {
Log.w("[Call] Failed to find existing chat room for local address [$localSipUri] and remote address [$remoteSipUri]")
var chatRoomParams: ChatRoomParams? = null
if (conference != null) {
val params = core.createDefaultChatRoomParams()
params.subject = conference.subject
params.backend = ChatRoomBackend.FlexisipChat
params.isGroupEnabled = true
chatRoomParams = params
}
chatRoom = core.searchChatRoom(
chatRoomParams,
localAddress,
null,
arrayOf(remoteSipAddress)
)
}
if (chatRoom == null) {
val chatRoomParams = core.createDefaultChatRoomParams()
if (conference != null) {
Log.w("[Call] Failed to find existing chat room with same subject & participants, creating it")
chatRoomParams.backend = ChatRoomBackend.FlexisipChat
chatRoomParams.isGroupEnabled = true
chatRoomParams.subject = conference.subject
val participants = arrayOfNulls<Address>(conference.participantCount)
val addresses = arrayListOf<Address>()
for (participant in conference.participantList) {
addresses.add(participant.address)
}
addresses.toArray(participants)
Log.i("[Call] Creating chat room with same subject [${chatRoomParams.subject}] & participants as for conference")
chatRoom = core.createChatRoom(chatRoomParams, localAddress, participants)
} else {
Log.w("[Call] Failed to find existing chat room with same participants, creating it")
// TODO: configure chat room params
chatRoom = core.createChatRoom(chatRoomParams, localAddress, arrayOf(remoteSipAddress))
}
}
if (chatRoom == null) {
Log.e("[Call] Failed to create a chat room for local address [$localSipUri] and remote address [$remoteSipUri]!")
}
} else {
Log.e("[Call] Failed to get either local [$localSipUri] or remote [$remoteSipUri] SIP address!")
}
}
private fun isCallPaused(): Boolean {
return when (call.state) {
Call.State.Paused, Call.State.Pausing -> true
else -> false
}
}
private fun isCallRemotelyPaused(): Boolean {
return when (call.state) {
Call.State.PausedByRemote -> {
val conference = call.conference
if (conference != null && conference.me.isFocus) {
Log.w("[Call] State is paused by remote but we are the focus of the conference, so considering call as active")
false
} else {
true
}
}
else -> false
}
}
private fun canCallBePaused(): Boolean {
return !call.mediaInProgress() && when (call.state) {
Call.State.StreamsRunning, Call.State.PausedByRemote -> true
else -> false
}
}
private fun update() {
isPaused.value = isCallPaused()
isRemotelyPaused.value = isCallRemotelyPaused()
canBePaused.value = canCallBePaused()
updateConferenceInfo()
isOutgoing.value = when (call.state) {
Call.State.OutgoingInit, Call.State.OutgoingEarlyMedia, Call.State.OutgoingProgress, Call.State.OutgoingRinging -> true
else -> false
}
isIncoming.value = when (call.state) {
Call.State.IncomingReceived, Call.State.IncomingEarlyMedia -> true
else -> false
}
// Check periodically until mediaInProgress is false
if (call.mediaInProgress()) {
scope.launch {
delay(1000)
update()
}
}
}
private fun updateConferenceInfo() {
val conference = call.conference
isInRemoteConference.value = conference != null
if (conference != null) {
remoteConferenceSubject.value = if (conference.subject.isNullOrEmpty()) {
if (conference.me.isFocus) {
AppUtils.getString(R.string.conference_local_title)
} else {
AppUtils.getString(R.string.conference_default_title)
}
} else {
conference.subject
}
Log.d("[Call] Found conference related to this call with subject [${remoteConferenceSubject.value}]")
}
}
private fun startVideoUpdateAcceptanceTimer() {
timer?.cancel()
timer = Timer("Call update timeout")
timer?.schedule(
object : TimerTask() {
override fun run() {
// Decline call update
coreContext.videoUpdateRequestTimedOut(call)
}
},
30000
)
Log.i("[Call] Starting 30 seconds timer to automatically decline video request")
}
private fun updateActiveAndNotInConference() {
isActiveAndNotInConference.value = isPaused.value == false && isRemotelyPaused.value == false && isInRemoteConference.value == false
}
}

View file

@ -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,7 +17,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.call.data
package org.linphone.activities.voip.data
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
@ -31,8 +31,6 @@ class CallStatisticsData(val call: Call) : GenericContactData(call.remoteAddress
val isVideoEnabled = MutableLiveData<Boolean>()
val isExpanded = MutableLiveData<Boolean>()
private val listener = object : CoreListenerStub() {
override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) {
if (call == this@CallStatisticsData.call) {
@ -52,8 +50,6 @@ class CallStatisticsData(val call: Call) : GenericContactData(call.remoteAddress
val videoEnabled = call.currentParams.isVideoEnabled
isVideoEnabled.value = videoEnabled
isExpanded.value = coreContext.core.currentCall == call
}
override fun destroy() {
@ -61,10 +57,6 @@ class CallStatisticsData(val call: Call) : GenericContactData(call.remoteAddress
super.destroy()
}
fun toggleExpanded() {
isExpanded.value = isExpanded.value != true
}
private fun initCallStats() {
val audioList = arrayListOf<StatItemData>()
audioList.add(StatItemData(StatType.CAPTURE))

View file

@ -0,0 +1,70 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.voip.data
import androidx.lifecycle.MutableLiveData
import org.linphone.contact.GenericContactData
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.LinphoneUtils
class ConferenceParticipantData(
val conference: Conference,
val participant: Participant
) :
GenericContactData(participant.address) {
val sipUri: String get() = LinphoneUtils.getDisplayableAddress(participant.address)
val isAdmin = MutableLiveData<Boolean>()
val isMeAdmin = MutableLiveData<Boolean>()
init {
isAdmin.value = participant.isAdmin
isMeAdmin.value = conference.me.isAdmin
Log.i("[Conference Participant] Participant ${participant.address.asStringUriOnly()} is ${if (participant.isAdmin) "admin" else "not admin"}")
}
fun setAdmin() {
if (conference.me.isAdmin) {
Log.i("[Conference Participant] Participant ${participant.address.asStringUriOnly()} will be set as admin")
conference.setParticipantAdminStatus(participant, true)
} else {
Log.e("[Conference Participant] You aren't admin, you can't change participants admin rights")
}
}
fun unsetAdmin() {
if (conference.me.isAdmin) {
Log.i("[Conference Participant] Participant ${participant.address.asStringUriOnly()} will be unset as admin")
conference.setParticipantAdminStatus(participant, false)
} else {
Log.e("[Conference Participant] You aren't admin, you can't change participants admin rights")
}
}
fun removeParticipantFromConference() {
if (conference.me.isAdmin) {
Log.i("[Conference Participant] Removing participant ${participant.address.asStringUriOnly()} from conference")
conference.removeParticipant(participant)
} else {
Log.e("[Conference Participant] Can't remove participant ${participant.address.asStringUriOnly()} from conference, you aren't admin")
}
}
}

View file

@ -0,0 +1,161 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.voip.data
import android.graphics.SurfaceTexture
import android.view.TextureView
import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.contact.GenericContactData
import org.linphone.core.MediaDirection
import org.linphone.core.ParticipantDevice
import org.linphone.core.ParticipantDeviceListenerStub
import org.linphone.core.StreamType
import org.linphone.core.tools.Log
class ConferenceParticipantDeviceData(
val participantDevice: ParticipantDevice,
val isMe: Boolean
) :
GenericContactData(participantDevice.address) {
val videoEnabled = MutableLiveData<Boolean>()
val activeSpeaker = MutableLiveData<Boolean>()
val isInConference = MutableLiveData<Boolean>()
private var textureView: TextureView? = null
private val listener = object : ParticipantDeviceListenerStub() {
override fun onIsSpeakingChanged(
participantDevice: ParticipantDevice,
isSpeaking: Boolean
) {
Log.i("[Conference Participant Device] Participant [${participantDevice.address.asStringUriOnly()}] is ${if (isSpeaking) "speaking" else "not speaking"}")
activeSpeaker.value = isSpeaking
}
override fun onConferenceJoined(participantDevice: ParticipantDevice) {
Log.i("[Conference Participant Device] Participant [${participantDevice.address.asStringUriOnly()}] has joined the conference")
isInConference.value = true
updateWindowId(textureView)
}
override fun onConferenceLeft(participantDevice: ParticipantDevice) {
Log.i("[Conference Participant Device] Participant [${participantDevice.address.asStringUriOnly()}] has left the conference")
isInConference.value = false
updateWindowId(null)
}
override fun onStreamCapabilityChanged(
participantDevice: ParticipantDevice,
direction: MediaDirection,
streamType: StreamType
) {
if (streamType == StreamType.Video) {
Log.i("[Conference Participant Device] Participant [${participantDevice.address.asStringUriOnly()}] video capability changed to $direction")
}
}
override fun onStreamAvailabilityChanged(
participantDevice: ParticipantDevice,
available: Boolean,
streamType: StreamType
) {
if (streamType == StreamType.Video) {
Log.i("[Conference Participant Device] Participant [${participantDevice.address.asStringUriOnly()}] video availability changed to ${if (available) "available" else "unavailable"}")
videoEnabled.value = available
if (available) {
updateWindowId(textureView)
}
}
}
}
init {
Log.i("[Conference Participant Device] Created device width Address [${participantDevice.address.asStringUriOnly()}], is it myself? $isMe")
participantDevice.addListener(listener)
activeSpeaker.value = false
videoEnabled.value = participantDevice.getStreamAvailability(StreamType.Video)
isInConference.value = participantDevice.isInConference
val videoCapability = participantDevice.getStreamCapability(StreamType.Video)
Log.i("[Conference Participant Device] Participant [${participantDevice.address.asStringUriOnly()}], is in conf? ${isInConference.value}, is video enabled? ${videoEnabled.value} ($videoCapability)")
}
override fun destroy() {
participantDevice.removeListener(listener)
super.destroy()
}
fun switchCamera() {
coreContext.switchCamera()
}
fun isSwitchCameraAvailable(): Boolean {
return isMe && coreContext.showSwitchCameraButton()
}
fun setTextureView(tv: TextureView) {
textureView = tv
if (tv.isAvailable) {
Log.i("[Conference Participant Device] Setting textureView [$textureView] for participant [${participantDevice.address.asStringUriOnly()}]")
updateWindowId(textureView)
} else {
Log.i("[Conference Participant Device] Got textureView [$textureView] for participant [${participantDevice.address.asStringUriOnly()}], but it is not available yet")
tv.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(
surface: SurfaceTexture,
width: Int,
height: Int
) {
Log.i("[Conference Participant Device] Setting textureView [$textureView] for participant [${participantDevice.address.asStringUriOnly()}]")
updateWindowId(textureView)
}
override fun onSurfaceTextureSizeChanged(
surface: SurfaceTexture,
width: Int,
height: Int
) { }
override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
Log.w("[Conference Participant Device] TextureView [$textureView] for participant [${participantDevice.address.asStringUriOnly()}] has been destroyed")
textureView = null
updateWindowId(null)
return true
}
override fun onSurfaceTextureUpdated(surface: SurfaceTexture) { }
}
}
}
private fun updateWindowId(windowId: Any?) {
if (isMe) {
coreContext.core.nativePreviewWindowId = windowId
} else {
participantDevice.nativeVideoWindowId = windowId
}
}
}

View file

@ -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,7 +17,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.call.data
package org.linphone.activities.voip.data
import androidx.lifecycle.MutableLiveData
import java.text.DecimalFormat

View file

@ -0,0 +1,338 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.voip.fragments
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import android.os.SystemClock
import android.view.View
import android.widget.Chronometer
import android.widget.LinearLayout
import android.widget.RelativeLayout
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.navigation.navGraphViewModels
import com.google.android.material.snackbar.Snackbar
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.activities.*
import org.linphone.activities.main.MainActivity
import org.linphone.activities.main.viewmodels.DialogViewModel
import org.linphone.activities.navigateToCallsList
import org.linphone.activities.navigateToConferenceParticipants
import org.linphone.activities.voip.viewmodels.CallsViewModel
import org.linphone.activities.voip.viewmodels.ConferenceViewModel
import org.linphone.activities.voip.viewmodels.ControlsViewModel
import org.linphone.activities.voip.viewmodels.StatisticsListViewModel
import org.linphone.activities.voip.views.RoundCornersTextureView
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.databinding.VoipActiveCallOrConferenceFragmentBindingImpl
import org.linphone.mediastream.video.capture.CaptureTextureView
import org.linphone.utils.AppUtils
import org.linphone.utils.DialogUtils
class ActiveCallOrConferenceFragment : GenericFragment<VoipActiveCallOrConferenceFragmentBindingImpl>() {
private val controlsViewModel: ControlsViewModel by navGraphViewModels(R.id.call_nav_graph)
private val callsViewModel: CallsViewModel by navGraphViewModels(R.id.call_nav_graph)
private val conferenceViewModel: ConferenceViewModel by navGraphViewModels(R.id.call_nav_graph)
private val statsViewModel: StatisticsListViewModel by navGraphViewModels(R.id.call_nav_graph)
private var dialog: Dialog? = null
override fun getLayoutId(): Int = R.layout.voip_active_call_or_conference_fragment
override fun onStart() {
useMaterialSharedAxisXForwardAnimation = false
super.onStart()
}
@SuppressLint("CutPasteId")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
controlsViewModel.hideCallStats() // In case it was toggled on during incoming/outgoing fragment was visible
binding.lifecycleOwner = viewLifecycleOwner
binding.controlsViewModel = controlsViewModel
binding.callsViewModel = callsViewModel
binding.conferenceViewModel = conferenceViewModel
binding.statsViewModel = statsViewModel
conferenceViewModel.conferenceMosaicDisplayMode.observe(
viewLifecycleOwner
) {
if (it) {
startTimer(R.id.grid_conference_timer)
}
}
conferenceViewModel.conferenceActiveSpeakerDisplayMode.observe(
viewLifecycleOwner
) {
if (it) {
startTimer(R.id.active_speaker_conference_timer)
if (conferenceViewModel.conferenceExists.value == true) {
Log.i("[Call] Local participant is in conference and current layout is active speaker, updating Core's native window id")
val layout =
binding.root.findViewById<RelativeLayout>(R.id.conference_active_speaker_layout)
val window =
layout?.findViewById<RoundCornersTextureView>(R.id.conference_active_speaker_remote_video)
coreContext.core.nativeVideoWindowId = window
} else {
Log.i("[Call] Either not in conference or current layout isn't active speaker, updating Core's native window id")
val layout = binding.root.findViewById<LinearLayout>(R.id.remote_layout)
val window =
layout?.findViewById<RoundCornersTextureView>(R.id.remote_video_surface)
coreContext.core.nativeVideoWindowId = window
}
}
}
conferenceViewModel.conferenceParticipantDevices.observe(
viewLifecycleOwner
) {
if (it.size > conferenceViewModel.maxParticipantsForMosaicLayout) {
showSnackBar(R.string.conference_too_many_participants_for_mosaic_layout)
}
}
conferenceViewModel.conference.observe(
viewLifecycleOwner
) { conference ->
if (corePreferences.enableFullScreenWhenJoiningVideoConference) {
if (conference != null && conference.currentParams.isVideoEnabled) {
if (conference.me.devices.find { it.getStreamAvailability(StreamType.Video) } != null) {
Log.i("[Call] Conference is video & our device has video enabled, enabling full screen mode")
controlsViewModel.fullScreenMode.value = true
}
}
}
}
callsViewModel.currentCallData.observe(
viewLifecycleOwner
) {
if (it != null) {
val timer = binding.root.findViewById<Chronometer>(R.id.active_call_timer)
timer.base =
SystemClock.elapsedRealtime() - (1000 * it.call.duration) // Linphone timestamps are in seconds
timer.start()
}
}
controlsViewModel.goToConferenceParticipantsListEvent.observe(
viewLifecycleOwner
) {
it.consume {
navigateToConferenceParticipants()
}
}
controlsViewModel.goToChatEvent.observe(
viewLifecycleOwner
) {
it.consume {
goToChat()
}
}
controlsViewModel.goToCallsListEvent.observe(
viewLifecycleOwner
) {
it.consume {
navigateToCallsList()
}
}
controlsViewModel.goToConferenceLayoutSettings.observe(
viewLifecycleOwner
) {
it.consume {
navigateToConferenceLayout()
}
}
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.isVideoEnabled) {
val remoteVideo = call.remoteParams?.isVideoEnabled ?: false
val localVideo = call.currentParams.isVideoEnabled
if (remoteVideo && !localVideo) {
showCallVideoUpdateDialog(call)
}
} else {
Log.w("[Call] Video display & capture are disabled, don't show video dialog")
}
}
val conference = call.conference
if (conference != null && conferenceViewModel.conference.value == null) {
Log.i("[Call] Found conference attached to call and no conference in dedicated view model, init & configure it")
conferenceViewModel.initConference(conference)
conferenceViewModel.configureConference(conference)
}
}
}
controlsViewModel.goToDialer.observe(
viewLifecycleOwner
) {
it.consume { isCallTransfer ->
val intent = Intent()
intent.setClass(requireContext(), MainActivity::class.java)
intent.putExtra("Dialer", true)
intent.putExtra("Transfer", isCallTransfer)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}
}
val remoteLayout = binding.root.findViewById<LinearLayout>(R.id.remote_layout)
val remoteVideoView = remoteLayout.findViewById<RoundCornersTextureView>(R.id.remote_video_surface)
coreContext.core.nativeVideoWindowId = remoteVideoView
val localVideoView = remoteLayout.findViewById<CaptureTextureView>(R.id.local_preview_video_surface)
coreContext.core.nativePreviewWindowId = localVideoView
binding.stubbedConferenceActiveSpeakerLayout.setOnInflateListener { _, inflated ->
Log.i("[Call] Active speaker conference layout inflated")
val binding = DataBindingUtil.bind<ViewDataBinding>(inflated)
binding?.lifecycleOwner = viewLifecycleOwner
startTimer(R.id.active_speaker_conference_timer)
}
binding.stubbedConferenceGridLayout.setOnInflateListener { _, inflated ->
Log.i("[Call] Mosaic conference layout inflated")
val binding = DataBindingUtil.bind<ViewDataBinding>(inflated)
binding?.lifecycleOwner = viewLifecycleOwner
startTimer(R.id.grid_conference_timer)
}
binding.stubbedAudioRoutes.setOnInflateListener { _, inflated ->
val binding = DataBindingUtil.bind<ViewDataBinding>(inflated)
binding?.lifecycleOwner = viewLifecycleOwner
}
binding.stubbedNumpad.setOnInflateListener { _, inflated ->
val binding = DataBindingUtil.bind<ViewDataBinding>(inflated)
binding?.lifecycleOwner = viewLifecycleOwner
}
binding.stubbedCallStats.setOnInflateListener { _, inflated ->
val binding = DataBindingUtil.bind<ViewDataBinding>(inflated)
binding?.lifecycleOwner = viewLifecycleOwner
}
binding.stubbedPausedCall.setOnInflateListener { _, inflated ->
val binding = DataBindingUtil.bind<ViewDataBinding>(inflated)
binding?.lifecycleOwner = viewLifecycleOwner
}
binding.stubbedRemotelyPausedCall.setOnInflateListener { _, inflated ->
val binding = DataBindingUtil.bind<ViewDataBinding>(inflated)
binding?.lifecycleOwner = viewLifecycleOwner
}
binding.stubbedPausedConference.setOnInflateListener { _, inflated ->
val binding = DataBindingUtil.bind<ViewDataBinding>(inflated)
binding?.lifecycleOwner = viewLifecycleOwner
}
}
override fun onPause() {
super.onPause()
controlsViewModel.hideExtraButtons(true)
}
override fun onDestroy() {
super.onDestroy()
coreContext.core.nativeVideoWindowId = null
coreContext.core.nativePreviewWindowId = null
}
private fun showCallVideoUpdateDialog(call: Call) {
val viewModel = DialogViewModel(AppUtils.getString(R.string.call_video_update_requested_dialog))
dialog = DialogUtils.getVoipDialog(requireContext(), viewModel)
viewModel.showCancelButton(
{
coreContext.answerCallVideoUpdateRequest(call, false)
dialog?.dismiss()
},
getString(R.string.dialog_decline)
)
viewModel.showOkButton(
{
coreContext.answerCallVideoUpdateRequest(call, true)
dialog?.dismiss()
},
getString(R.string.dialog_accept)
)
dialog?.show()
}
private fun goToChat() {
val intent = Intent()
intent.setClass(requireContext(), MainActivity::class.java)
intent.putExtra("Chat", true)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}
private fun showSnackBar(resourceId: Int) {
Snackbar.make(binding.coordinator, resourceId, Snackbar.LENGTH_LONG).show()
}
private fun startTimer(timerId: Int) {
val timer: Chronometer? = binding.root.findViewById(timerId)
if (timer == null) {
Log.w("[Call] Timer not found, maybe view wasn't inflated yet?")
return
}
val conference = conferenceViewModel.conference.value
if (conference != null) {
val duration = 1000 * conference.duration // Linphone timestamps are in seconds
timer.base = SystemClock.elapsedRealtime() - duration
} else {
Log.e("[Call] Conference not found, timer will have no base")
}
timer.start()
}
}

View file

@ -0,0 +1,160 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.voip.fragments
import android.content.Intent
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.widget.PopupWindow
import androidx.databinding.DataBindingUtil
import androidx.navigation.navGraphViewModels
import org.linphone.R
import org.linphone.activities.GenericFragment
import org.linphone.activities.main.MainActivity
import org.linphone.activities.voip.data.CallData
import org.linphone.activities.voip.viewmodels.CallsViewModel
import org.linphone.activities.voip.viewmodels.ConferenceViewModel
import org.linphone.databinding.VoipCallContextMenuBindingImpl
import org.linphone.databinding.VoipCallsListFragmentBinding
import org.linphone.utils.AppUtils
class CallsListFragment : GenericFragment<VoipCallsListFragmentBinding>() {
private val callsViewModel: CallsViewModel by navGraphViewModels(R.id.call_nav_graph)
private val conferenceViewModel: ConferenceViewModel by navGraphViewModels(R.id.call_nav_graph)
override fun getLayoutId(): Int = R.layout.voip_calls_list_fragment
private val callContextMenuClickListener = object : CallData.CallContextMenuClickListener {
override fun onShowContextMenu(anchor: View, callData: CallData) {
showCallMenu(anchor, callData)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.callsViewModel = callsViewModel
binding.conferenceViewModel = conferenceViewModel
binding.setCancelClickListener {
goBack()
}
binding.setAddCallClickListener {
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)
}
callsViewModel.callsData.observe(
viewLifecycleOwner
) {
for (data in it) {
data.contextMenuClickListener = callContextMenuClickListener
}
}
}
private fun showCallMenu(anchor: View, callData: CallData) {
val popupView: VoipCallContextMenuBindingImpl = DataBindingUtil.inflate(
LayoutInflater.from(requireContext()),
R.layout.voip_call_context_menu, null, false
)
val itemSize = AppUtils.getDimension(R.dimen.voip_call_context_menu_item_height).toInt()
var totalSize = itemSize * 5
if (callData.isPaused.value == true ||
callData.isIncoming.value == true ||
callData.isOutgoing.value == true ||
callData.isInRemoteConference.value == true
) {
popupView.hidePause = true
totalSize -= itemSize
}
if (callData.isIncoming.value == true ||
callData.isOutgoing.value == true ||
callData.isInRemoteConference.value == true
) {
popupView.hideResume = true
popupView.hideTransfer = true
totalSize -= itemSize * 2
} else if (callData.isPaused.value == false) {
popupView.hideResume = true
totalSize -= itemSize
}
if (callData.isIncoming.value == false) {
popupView.hideAccept = 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
val popupWindow = PopupWindow(
popupView.root,
AppUtils.getDimension(R.dimen.voip_call_context_menu_width).toInt(),
totalSize,
true
)
// Elevation is for showing a shadow around the popup
popupWindow.elevation = 20f
popupView.setResumeClickListener {
callData.resume()
popupWindow.dismiss()
}
popupView.setPauseClickListener {
callData.pause()
popupWindow.dismiss()
}
popupView.setTransferClickListener {
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)
popupWindow.dismiss()
}
popupView.setAnswerClickListener {
callData.accept()
popupWindow.dismiss()
}
popupView.setHangupClickListener {
callData.terminate()
popupWindow.dismiss()
}
popupWindow.showAsDropDown(anchor, 0, 0, Gravity.END or Gravity.TOP)
}
}

View file

@ -0,0 +1,263 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.voip.fragments
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Parcelable
import android.provider.MediaStore
import android.view.View
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.activities.GenericFragment
import org.linphone.activities.main.MainActivity
import org.linphone.activities.main.chat.adapters.ChatMessagesListAdapter
import org.linphone.activities.main.chat.data.ChatMessageData
import org.linphone.activities.main.chat.viewmodels.*
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
import org.linphone.core.ChatRoom
import org.linphone.core.Factory
import org.linphone.core.tools.Log
import org.linphone.databinding.VoipChatFragmentBinding
import org.linphone.utils.FileUtils
import org.linphone.utils.PermissionHelper
class ChatFragment : GenericFragment<VoipChatFragmentBinding>() {
private lateinit var adapter: ChatMessagesListAdapter
private lateinit var viewModel: ChatRoomViewModel
private lateinit var listViewModel: ChatMessagesListViewModel
private lateinit var chatSendingViewModel: ChatMessageSendingViewModel
override fun getLayoutId(): Int = R.layout.voip_chat_fragment
private val observer = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (positionStart == adapter.itemCount - itemCount) {
adapter.notifyItemChanged(positionStart - 1) // For grouping purposes
scrollToBottom()
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.setCancelClickListener {
goBack()
}
binding.setChatRoomsListClickListener {
val intent = Intent()
intent.setClass(requireContext(), MainActivity::class.java)
intent.putExtra("Chat", true)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}
binding.setAttachFileClickListener {
if (PermissionHelper.get().hasReadExternalStoragePermission() && PermissionHelper.get().hasCameraPermission()) {
pickFile()
} else {
Log.i("[Chat] Asking for READ_EXTERNAL_STORAGE and CAMERA permissions")
requestPermissions(
arrayOf(
android.Manifest.permission.READ_EXTERNAL_STORAGE,
android.Manifest.permission.CAMERA
),
0
)
}
}
val localSipUri = arguments?.getString("LocalSipUri")
val remoteSipUri = arguments?.getString("RemoteSipUri")
var chatRoom: ChatRoom? = null
if (localSipUri != null && remoteSipUri != null) {
Log.i("[Chat] Found local [$localSipUri] & remote [$remoteSipUri] addresses in arguments")
val localAddress = Factory.instance().createAddress(localSipUri)
val remoteSipAddress = Factory.instance().createAddress(remoteSipUri)
chatRoom = coreContext.core.searchChatRoom(null, localAddress, remoteSipAddress, arrayOfNulls(0))
}
chatRoom ?: return
viewModel = requireActivity().run {
ViewModelProvider(
this,
ChatRoomViewModelFactory(chatRoom)
)[ChatRoomViewModel::class.java]
}
binding.viewModel = viewModel
listViewModel = ViewModelProvider(
this,
ChatMessagesListViewModelFactory(chatRoom)
)[ChatMessagesListViewModel::class.java]
chatSendingViewModel = ViewModelProvider(
this,
ChatMessageSendingViewModelFactory(chatRoom)
)[ChatMessageSendingViewModel::class.java]
binding.chatSendingViewModel = chatSendingViewModel
val listSelectionViewModel = ViewModelProvider(this)[ListTopBarViewModel::class.java]
adapter = ChatMessagesListAdapter(listSelectionViewModel, this)
// SubmitList is done on a background thread
// We need this adapter data observer to know when to scroll
binding.chatMessagesList.adapter = adapter
adapter.registerAdapterDataObserver(observer)
// Disable context menu on each message
adapter.disableAdvancedContextMenuOptions()
val layoutManager = LinearLayoutManager(requireContext())
layoutManager.stackFromEnd = true
binding.chatMessagesList.layoutManager = layoutManager
listViewModel.events.observe(
viewLifecycleOwner
) { events ->
adapter.submitList(events)
}
chatSendingViewModel.textToSend.observe(
viewLifecycleOwner
) {
chatSendingViewModel.onTextToSendChanged(it)
}
adapter.replyMessageEvent.observe(
viewLifecycleOwner
) {
it.consume { chatMessage ->
chatSendingViewModel.pendingChatMessageToReplyTo.value?.destroy()
chatSendingViewModel.pendingChatMessageToReplyTo.value =
ChatMessageData(chatMessage)
chatSendingViewModel.isPendingAnswer.value = true
}
}
}
override fun onResume() {
super.onResume()
if (this::viewModel.isInitialized) {
// Prevent notifications for this chat room to be displayed
val peerAddress = viewModel.chatRoom.peerAddress.asStringUriOnly()
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = peerAddress
viewModel.chatRoom.markAsRead()
} else {
Log.e("[Chat] Fragment resuming but viewModel lateinit property isn't initialized!")
}
}
override fun onPause() {
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
super.onPause()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) {
lifecycleScope.launch {
for (
fileToUploadPath in FileUtils.getFilesPathFromPickerIntent(
data,
chatSendingViewModel.temporaryFileUploadPath
)
) {
chatSendingViewModel.addAttachment(fileToUploadPath)
}
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
var atLeastOneGranted = false
for (result in grantResults) {
atLeastOneGranted = atLeastOneGranted || result == PackageManager.PERMISSION_GRANTED
}
when (requestCode) {
0 -> {
if (atLeastOneGranted) {
pickFile()
}
}
}
}
private fun scrollToBottom() {
if (adapter.itemCount > 0) {
binding.chatMessagesList.scrollToPosition(adapter.itemCount - 1)
}
}
private fun pickFile() {
val intentsList = ArrayList<Intent>()
val pickerIntent = Intent(Intent.ACTION_GET_CONTENT)
pickerIntent.type = "*/*"
pickerIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
if (PermissionHelper.get().hasCameraPermission()) {
// Allows to capture directly from the camera
val capturePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
val tempFileName = System.currentTimeMillis().toString() + ".jpeg"
val file = FileUtils.getFileStoragePath(tempFileName)
chatSendingViewModel.temporaryFileUploadPath = file
val publicUri = FileProvider.getUriForFile(
requireContext(),
requireContext().getString(R.string.file_provider),
file
)
capturePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, publicUri)
capturePictureIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
capturePictureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intentsList.add(capturePictureIntent)
val captureVideoIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
intentsList.add(captureVideoIntent)
}
val chooserIntent =
Intent.createChooser(pickerIntent, getString(R.string.chat_message_pick_file_dialog))
chooserIntent.putExtra(
Intent.EXTRA_INITIAL_INTENTS,
intentsList.toArray(arrayOf<Parcelable>())
)
startActivityForResult(chooserIntent, 0)
}
}

View file

@ -0,0 +1,133 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.voip.fragments
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModelProvider
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.voip.viewmodels.ConferenceParticipantsViewModel
import org.linphone.activities.voip.viewmodels.ConferenceParticipantsViewModelFactory
import org.linphone.activities.voip.viewmodels.ConferenceViewModel
import org.linphone.contact.ContactsSelectionAdapter
import org.linphone.core.tools.Log
import org.linphone.databinding.VoipConferenceParticipantsAddFragmentBinding
import org.linphone.utils.AppUtils
import org.linphone.utils.PermissionHelper
class ConferenceAddParticipantsFragment : GenericFragment<VoipConferenceParticipantsAddFragmentBinding>() {
private val conferenceViewModel: ConferenceViewModel by navGraphViewModels(R.id.call_nav_graph)
private lateinit var viewModel: ConferenceParticipantsViewModel
private lateinit var adapter: ContactsSelectionAdapter
override fun getLayoutId(): Int = R.layout.voip_conference_participants_add_fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
val conference = conferenceViewModel.conference.value
conference ?: return
viewModel = ViewModelProvider(
this,
ConferenceParticipantsViewModelFactory(conference)
)[ConferenceParticipantsViewModel::class.java]
binding.viewModel = viewModel
adapter = ContactsSelectionAdapter(viewLifecycleOwner)
adapter.setLimeCapabilityRequired(false) // TODO: Use right value from conference
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.setApplyClickListener {
viewModel.applyChanges()
goBack()
}
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 Add Participants] Asking for READ_CONTACTS permission")
requestPermissions(arrayOf(android.Manifest.permission.READ_CONTACTS), 0)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
if (requestCode == 0) {
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
if (granted) {
Log.i("[Conference Add Participants] READ_CONTACTS permission granted")
coreContext.contactsManager.onReadContactsPermissionGranted()
coreContext.contactsManager.fetchContactsAsync()
} else {
Log.w("[Conference Add Participants] READ_CONTACTS permission denied")
}
}
}
}

View file

@ -0,0 +1,103 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.voip.fragments
import android.os.Bundle
import android.view.View
import android.widget.LinearLayout
import androidx.navigation.navGraphViewModels
import org.linphone.R
import org.linphone.activities.GenericFragment
import org.linphone.activities.voip.viewmodels.ConferenceViewModel
import org.linphone.core.ConferenceLayout
import org.linphone.core.tools.Log
import org.linphone.databinding.VoipConferenceLayoutFragmentBinding
class ConferenceLayoutFragment : GenericFragment<VoipConferenceLayoutFragmentBinding>() {
private val conferenceViewModel: ConferenceViewModel by navGraphViewModels(R.id.call_nav_graph)
override fun getLayoutId(): Int = R.layout.voip_conference_layout_fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.conferenceViewModel = conferenceViewModel
binding.setCancelClickListener {
goBack()
}
conferenceViewModel.conferenceMosaicDisplayMode.observe(
viewLifecycleOwner
) {
if (it) {
Log.i("[Conference] Trying to change conference layout to Grid")
val conference = conferenceViewModel.conference.value
if (conference != null) {
conference.layout = ConferenceLayout.Grid
} else {
Log.e("[Conference] Conference is null in ConferenceViewModel")
}
}
}
conferenceViewModel.conferenceActiveSpeakerDisplayMode.observe(
viewLifecycleOwner
) {
if (it) {
Log.i("[Conference] Trying to change conference layout to ActiveSpeaker")
val conference = conferenceViewModel.conference.value
if (conference != null) {
conference.layout = ConferenceLayout.ActiveSpeaker
} else {
Log.e("[Conference] Conference is null in ConferenceViewModel")
}
}
}
conferenceViewModel.conferenceParticipantDevices.observe(
viewLifecycleOwner
) {
if (it.size > conferenceViewModel.maxParticipantsForMosaicLayout) {
showTooManyParticipantsForMosaicLayoutDialog()
}
}
binding.setDismissDialogClickListener {
val dialog = binding.root.findViewById<LinearLayout>(R.id.too_many_participants_dialog)
dialog?.visibility = View.GONE
}
}
override fun onResume() {
super.onResume()
if (conferenceViewModel.conferenceParticipantDevices.value.orEmpty().size > conferenceViewModel.maxParticipantsForMosaicLayout) {
showTooManyParticipantsForMosaicLayoutDialog()
}
}
private fun showTooManyParticipantsForMosaicLayoutDialog() {
val dialog = binding.root.findViewById<LinearLayout>(R.id.too_many_participants_dialog)
dialog?.visibility = View.VISIBLE
}
}

View file

@ -0,0 +1,80 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.voip.fragments
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.navigation.navGraphViewModels
import org.linphone.R
import org.linphone.activities.GenericFragment
import org.linphone.activities.voip.viewmodels.CallsViewModel
import org.linphone.activities.voip.viewmodels.ConferenceViewModel
import org.linphone.core.tools.Log
import org.linphone.databinding.VoipConferenceParticipantsFragmentBinding
class ConferenceParticipantsFragment : GenericFragment<VoipConferenceParticipantsFragmentBinding>() {
private val callsViewModel: CallsViewModel by navGraphViewModels(R.id.call_nav_graph)
private val conferenceViewModel: ConferenceViewModel by navGraphViewModels(R.id.call_nav_graph)
override fun getLayoutId(): Int = R.layout.voip_conference_participants_fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.callsViewModel = callsViewModel
binding.conferenceViewModel = conferenceViewModel
conferenceViewModel.conferenceExists.observe(
viewLifecycleOwner
) { exists ->
if (!exists) {
Log.w("[Conference Participants] Conference no longer exists, going back")
goBack()
}
}
conferenceViewModel.participantAdminStatusChangedEvent.observe(
viewLifecycleOwner
) {
it.consume { participantData ->
val participantName =
participantData.contact.value?.fullName ?: participantData.displayName.value
val message = if (participantData.participant.isAdmin) {
getString(R.string.conference_admin_set).format(participantName)
} else {
getString(R.string.conference_admin_unset).format(participantName)
}
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
binding.setCancelClickListener {
goBack()
}
binding.setEditClickListener {
// TODO: go to conferences view outside of call activity in edition mode
}
}
}

View file

@ -0,0 +1,84 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.activities.voip.fragments
import android.os.Bundle
import android.os.SystemClock
import android.view.View
import android.widget.Chronometer
import androidx.navigation.navGraphViewModels
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.activities.GenericFragment
import org.linphone.activities.navigateToActiveCall
import org.linphone.activities.voip.viewmodels.CallsViewModel
import org.linphone.activities.voip.viewmodels.ControlsViewModel
import org.linphone.core.tools.Log
import org.linphone.databinding.VoipCallIncomingFragmentBinding
class IncomingCallFragment : GenericFragment<VoipCallIncomingFragmentBinding>() {
private val controlsViewModel: ControlsViewModel by navGraphViewModels(R.id.call_nav_graph)
private val callsViewModel: CallsViewModel by navGraphViewModels(R.id.call_nav_graph)
override fun getLayoutId(): Int = R.layout.voip_call_incoming_fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.controlsViewModel = controlsViewModel
binding.callsViewModel = callsViewModel
callsViewModel.callConnectedEvent.observe(
viewLifecycleOwner
) {
it.consume {
navigateToActiveCall()
}
}
callsViewModel.callEndedEvent.observe(
viewLifecycleOwner
) {
it.consume {
navigateToActiveCall()
}
}
callsViewModel.currentCallData.observe(
viewLifecycleOwner
) {
if (it != null) {
val timer = binding.root.findViewById<Chronometer>(R.id.incoming_call_timer)
timer.base =
SystemClock.elapsedRealtime() - (1000 * it.call.duration) // Linphone timestamps are in seconds
timer.start()
}
}
val earlyMediaVideo = arguments?.getBoolean("earlyMediaVideo") ?: false
if (earlyMediaVideo) {
Log.i("[Incoming Call] Video early media detected, setting native window id")
coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface
}
}
}

Some files were not shown because too many files have changed in this diff Show more