New call/conference UI
This commit is contained in:
parent
51cf7a6711
commit
6ef3dc288e
468 changed files with 13721 additions and 5940 deletions
|
@ -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'
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -119,7 +119,7 @@ class ChatBubbleActivity : GenericActivity() {
|
|||
adapter.registerAdapterDataObserver(observer)
|
||||
|
||||
// Disable context menu on each message
|
||||
adapter.disableContextMenu()
|
||||
adapter.disableAdvancedContextMenuOptions()
|
||||
|
||||
adapter.openContentEvent.observe(
|
||||
this
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?)
|
||||
}
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,6 @@ class DevicesListChildData(private val device: ParticipantDevice) {
|
|||
}
|
||||
|
||||
fun onClick() {
|
||||
coreContext.startCall(device.address, true)
|
||||
coreContext.startCall(device.address, forceZRTP = true)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -175,7 +175,7 @@ class GroupInfoFragment : SecureFragment<ChatRoomGroupInfoFragmentBinding>() {
|
|||
dialog.show()
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewModel.onMessageToNotifyEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { messageResourceId ->
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -177,5 +177,11 @@ class SettingsFragment : SecureFragment<SettingsFragmentBinding>() {
|
|||
navigateToAdvancedSettings(binding.slidingPane)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.conferencesSettingsListener = object : SettingListenerStub() {
|
||||
override fun onClicked() {
|
||||
navigateToConferencesSettings(binding.slidingPane)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>>()
|
||||
}
|
|
@ -101,7 +101,6 @@ class TabsViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
override fun onCleared() {
|
||||
if (corePreferences.enableAnimations) bounceAnimator.end()
|
||||
coreContext.core.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
|
260
app/src/main/java/org/linphone/activities/voip/CallActivity.kt
Normal file
260
app/src/main/java/org/linphone/activities/voip/CallActivity.kt
Normal 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)*/
|
||||
}
|
||||
}
|
314
app/src/main/java/org/linphone/activities/voip/data/CallData.kt
Normal file
314
app/src/main/java/org/linphone/activities/voip/data/CallData.kt
Normal 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
|
||||
}
|
||||
}
|
|
@ -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))
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in a new issue