diff --git a/app/build.gradle b/app/build.gradle index effc1d64e..ac6879953 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -202,7 +202,7 @@ dependencies { implementation 'androidx.fragment:fragment-ktx:1.4.1' implementation 'androidx.core:core-ktx:1.7.0' - def nav_version = "2.4.0" + def nav_version = "2.4.1" implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" @@ -211,7 +211,7 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation "androidx.security:security-crypto-ktx:1.1.0-alpha03" implementation 'androidx.core:core-splashscreen:1.0.0-beta01' @@ -237,7 +237,7 @@ dependencies { implementation 'com.google.firebase:firebase-messaging' } - implementation 'org.linphone:linphone-sdk-android:5.1+' + implementation 'org.linphone:linphone-sdk-android:5.2+' // Only enable leak canary prior to release //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 88547b8ef..571c013c6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -115,19 +115,11 @@ - - - - - + 0 0 + 0
diff --git a/app/src/main/assets/assistant_linphone_default_values b/app/src/main/assets/assistant_linphone_default_values index 9f2badee0..497e98efc 100644 --- a/app/src/main/assets/assistant_linphone_default_values +++ b/app/src/main/assets/assistant_linphone_default_values @@ -15,8 +15,10 @@ nat_policy_default_values sip.linphone.org sip:conference-factory@sip.linphone.org + sip:videoconference-factory2@sip.linphone.org 1 1 + 1
stun.linphone.org diff --git a/app/src/main/assets/linphonerc_default b/app/src/main/assets/linphonerc_default index 48add4247..2e8b8b02e 100644 --- a/app/src/main/assets/linphonerc_default +++ b/app/src/main/assets/linphonerc_default @@ -33,6 +33,7 @@ file_transfer_server_url=https://www.linphone.org:444/lft.php version_check_url_root=https://www.linphone.org/releases max_calls=10 history_max_size=100 +conference_layout=1 [in-app-purchase] server_url=https://subscribe.linphone.org:444/inapp.php diff --git a/app/src/main/java/org/linphone/activities/Navigation.kt b/app/src/main/java/org/linphone/activities/Navigation.kt index 5d6bd190a..331c2417f 100644 --- a/app/src/main/java/org/linphone/activities/Navigation.kt +++ b/app/src/main/java/org/linphone/activities/Navigation.kt @@ -36,15 +36,22 @@ import org.linphone.activities.main.chat.fragments.ChatRoomCreationFragment import org.linphone.activities.main.chat.fragments.DetailChatRoomFragment import org.linphone.activities.main.chat.fragments.GroupInfoFragment import org.linphone.activities.main.chat.fragments.MasterChatRoomsFragment +import org.linphone.activities.main.conference.fragments.* import org.linphone.activities.main.contact.fragments.ContactEditorFragment import org.linphone.activities.main.contact.fragments.DetailContactFragment import org.linphone.activities.main.contact.fragments.MasterContactsFragment import org.linphone.activities.main.dialer.fragments.DialerFragment import org.linphone.activities.main.fragments.TabsFragment import org.linphone.activities.main.history.fragments.DetailCallLogFragment +import org.linphone.activities.main.history.fragments.DetailConferenceCallLogFragment import org.linphone.activities.main.history.fragments.MasterCallLogsFragment import org.linphone.activities.main.settings.fragments.* import org.linphone.activities.main.sidemenu.fragments.SideMenuFragment +import org.linphone.activities.voip.CallActivity +import org.linphone.activities.voip.fragments.ActiveCallOrConferenceFragment +import org.linphone.activities.voip.fragments.ConferenceParticipantsFragment +import org.linphone.activities.voip.fragments.IncomingCallFragment +import org.linphone.activities.voip.fragments.OutgoingCallFragment import org.linphone.contact.NativeContact import org.linphone.core.Address @@ -173,6 +180,104 @@ internal fun DialerFragment.navigateToConfigFileViewer() { ) } +internal fun DialerFragment.navigateToConferenceScheduling() { + findMasterNavController().navigate( + R.id.action_global_conferenceSchedulingFragment, + null, + popupTo() + ) +} + +/* Conference scheduling related */ + +internal fun ConferenceSchedulingFragment.navigateToParticipantsList() { + if (findNavController().currentDestination?.id == R.id.conferenceSchedulingFragment) { + findNavController().navigate( + R.id.action_conferenceSchedulingFragment_to_conferenceSchedulingParticipantsListFragment, + null, + popupTo(R.id.conferenceSchedulingParticipantsListFragment, true) + ) + } +} + +internal fun ConferenceSchedulingParticipantsListFragment.navigateToSummary() { + if (findNavController().currentDestination?.id == R.id.conferenceSchedulingParticipantsListFragment) { + findNavController().navigate( + R.id.action_conferenceSchedulingParticipantsListFragment_to_conferenceSchedulingSummaryFragment, + null, + popupTo(R.id.conferenceSchedulingSummaryFragment, true) + ) + } +} + +internal fun ConferenceSchedulingSummaryFragment.goToScheduledConferences() { + if (findNavController().currentDestination?.id == R.id.conferenceSchedulingSummaryFragment) { + findNavController().navigate( + R.id.action_global_scheduledConferencesFragment, + null, + popupTo(R.id.dialerFragment, false) + ) + } +} + +internal fun ConferenceSchedulingSummaryFragment.navigateToConferenceWaitingRoom( + address: String, + subject: String? +) { + val bundle = Bundle() + bundle.putString("Address", address) + bundle.putString("Subject", subject) + findMasterNavController().navigate( + R.id.action_global_conferenceWaitingRoomFragment, + bundle, + popupTo(R.id.dialerFragment, false) + ) +} + +internal fun ConferenceWaitingRoomFragment.navigateToDialer() { + findNavController().navigate( + R.id.action_global_dialerFragment, + null, + popupTo(R.id.dialerFragment, true) + ) +} + +internal fun DetailChatRoomFragment.navigateToConferenceWaitingRoom( + address: String, + subject: String? +) { + val bundle = Bundle() + bundle.putString("Address", address) + bundle.putString("Subject", subject) + findMasterNavController().navigate( + R.id.action_global_conferenceWaitingRoomFragment, + bundle, + popupTo(R.id.conferenceWaitingRoomFragment, true) + ) +} + +internal fun ScheduledConferencesFragment.navigateToConferenceWaitingRoom( + address: String, + subject: String? +) { + val bundle = Bundle() + bundle.putString("Address", address) + bundle.putString("Subject", subject) + findMasterNavController().navigate( + R.id.action_global_conferenceWaitingRoomFragment, + bundle, + popupTo(R.id.conferenceWaitingRoomFragment, true) + ) +} + +internal fun ScheduledConferencesFragment.navigateToConferenceScheduling() { + findMasterNavController().navigate( + R.id.action_global_conferenceSchedulingFragment, + null, + popupTo(R.id.conferenceSchedulingFragment, true) + ) +} + /* Chat related */ internal fun MasterChatRoomsFragment.navigateToChatRoom(args: Bundle) { @@ -499,6 +604,19 @@ internal fun MasterCallLogsFragment.navigateToCallHistory(slidingPane: SlidingPa } } +internal fun MasterCallLogsFragment.navigateToConferenceCallHistory(slidingPane: SlidingPaneLayout) { + if (findNavController().currentDestination?.id == R.id.masterCallLogsFragment) { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.history_nav_container) as NavHostFragment + navHostFragment.navController.navigate( + R.id.action_global_detailConferenceCallLogFragment, + null, + popupTo(R.id.detailConferenceCallLogFragment, true) + ) + if (!slidingPane.isOpen) slidingPane.openPane() + } +} + internal fun MasterCallLogsFragment.clearDisplayedCallHistory() { if (findNavController().currentDestination?.id == R.id.masterCallLogsFragment) { val navHostFragment = @@ -519,6 +637,20 @@ internal fun MasterCallLogsFragment.navigateToDialer(args: Bundle?) { ) } +internal fun MasterCallLogsFragment.navigateToConferenceWaitingRoom( + address: String, + subject: String? +) { + val bundle = Bundle() + bundle.putString("Address", address) + bundle.putString("Subject", subject) + findMasterNavController().navigate( + R.id.action_global_conferenceWaitingRoomFragment, + bundle, + popupTo(R.id.dialerFragment, false) + ) +} + internal fun DetailCallLogFragment.navigateToContacts(sipUriToAdd: String) { val deepLink = "linphone-android://contact/new/$sipUriToAdd" findMasterNavController().navigate(Uri.parse(deepLink)) @@ -564,6 +696,16 @@ internal fun DetailCallLogFragment.navigateToEmptyCallHistory() { } } +internal fun DetailConferenceCallLogFragment.navigateToEmptyCallHistory() { + if (findNavController().currentDestination?.id == R.id.detailConferenceCallLogFragment) { + findNavController().navigate( + R.id.action_global_emptyFragment, + null, + popupTo(R.id.emptyCallHistoryFragment, true) + ) + } +} + /* Settings related */ internal fun SettingsFragment.navigateToAccountSettings(identity: String) { @@ -683,6 +825,19 @@ internal fun SettingsFragment.navigateToAdvancedSettings(slidingPane: SlidingPan } } +internal fun SettingsFragment.navigateToConferencesSettings(slidingPane: SlidingPaneLayout) { + if (findNavController().currentDestination?.id == R.id.settingsFragment) { + val navHostFragment = + childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment + navHostFragment.navController.navigate( + R.id.action_global_conferencesSettingsFragment, + null, + popupTo(R.id.conferencesSettingsFragment, true) + ) + if (!slidingPane.isOpen) slidingPane.openPane() + } +} + internal fun AccountSettingsFragment.navigateToPhoneLinking(args: Bundle?) { if (findNavController().currentDestination?.id == R.id.accountSettingsFragment) { findNavController().navigate( @@ -731,6 +886,10 @@ internal fun ChatSettingsFragment.navigateToEmptySetting() { navigateToEmptySetting(findNavController()) } +internal fun ConferencesSettingsFragment.navigateToEmptySetting() { + navigateToEmptySetting(findNavController()) +} + internal fun ContactsSettingsFragment.navigateToEmptySetting() { navigateToEmptySetting(findNavController()) } @@ -778,6 +937,110 @@ internal fun SideMenuFragment.navigateToRecordings() { ) } +internal fun SideMenuFragment.navigateToScheduledConferences() { + findNavController().navigate( + R.id.action_global_scheduledConferencesFragment, + null, + popupTo(R.id.scheduledConferencesFragment, true) + ) +} + +/* Calls related */ + +internal fun CallActivity.navigateToActiveCall() { + if (findNavController(R.id.nav_host_fragment).currentDestination?.id != R.id.activeCallOrConferenceFragment) { + findNavController(R.id.nav_host_fragment).navigate( + R.id.action_global_activeCallOrConferenceFragment, + null, + popupTo(R.id.activeCallOrConferenceFragment, false) + ) + } +} + +internal fun CallActivity.navigateToOutgoingCall() { + findNavController(R.id.nav_host_fragment).navigate( + R.id.action_global_outgoingCallFragment, + null, + popupTo(R.id.activeCallOrConferenceFragment, false) + ) +} + +internal fun CallActivity.navigateToIncomingCall(earlyMediaVideoEnabled: Boolean) { + val args = Bundle() + args.putBoolean("earlyMediaVideo", earlyMediaVideoEnabled) + findNavController(R.id.nav_host_fragment).navigate( + R.id.action_global_incomingCallFragment, + args, + popupTo(R.id.activeCallOrConferenceFragment, false) + ) +} + +internal fun OutgoingCallFragment.navigateToActiveCall() { + findNavController().navigate( + R.id.action_global_activeCallOrConferenceFragment, + null, + popupTo(R.id.activeCallOrConferenceFragment, false) + ) +} + +internal fun IncomingCallFragment.navigateToActiveCall() { + findNavController().navigate( + R.id.action_global_activeCallOrConferenceFragment, + null, + popupTo(R.id.activeCallOrConferenceFragment, false) + ) +} + +internal fun ActiveCallOrConferenceFragment.navigateToCallsList() { + if (findNavController().currentDestination?.id == R.id.activeCallOrConferenceFragment) { + findNavController().navigate( + R.id.action_activeCallOrConferenceFragment_to_callsListFragment, + null, + popupTo() + ) + } +} + +internal fun ActiveCallOrConferenceFragment.navigateToConferenceParticipants() { + if (findNavController().currentDestination?.id == R.id.activeCallOrConferenceFragment) { + findNavController().navigate( + R.id.action_activeCallOrConferenceFragment_to_conferenceParticipantsFragment, + null, + popupTo() + ) + } +} + +internal fun ActiveCallOrConferenceFragment.navigateToChat(args: Bundle) { + if (findNavController().currentDestination?.id == R.id.activeCallOrConferenceFragment) { + findNavController().navigate( + R.id.action_activeCallOrConferenceFragment_to_chatFragment, + args, + popupTo() + ) + } +} + +internal fun ActiveCallOrConferenceFragment.navigateToConferenceLayout() { + if (findNavController().currentDestination?.id == R.id.activeCallOrConferenceFragment) { + findNavController().navigate( + R.id.action_activeCallOrConferenceFragment_to_conferenceLayoutFragment, + null, + popupTo() + ) + } +} + +internal fun ConferenceParticipantsFragment.navigateToAddParticipants() { + if (findNavController().currentDestination?.id == R.id.conferenceParticipantsFragment) { + findNavController().navigate( + R.id.action_conferenceParticipantsFragment_to_conferenceAddParticipantsFragment, + null, + popupTo(R.id.conferenceAddParticipantsFragment, true) + ) + } +} + /* Assistant related */ internal fun WelcomeFragment.navigateToEmailAccountCreation() { diff --git a/app/src/main/java/org/linphone/activities/call/ProximitySensorActivity.kt b/app/src/main/java/org/linphone/activities/ProximitySensorActivity.kt similarity index 95% rename from app/src/main/java/org/linphone/activities/call/ProximitySensorActivity.kt rename to app/src/main/java/org/linphone/activities/ProximitySensorActivity.kt index cc502b4d1..e66befe35 100644 --- a/app/src/main/java/org/linphone/activities/call/ProximitySensorActivity.kt +++ b/app/src/main/java/org/linphone/activities/ProximitySensorActivity.kt @@ -17,14 +17,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.activities.call +package org.linphone.activities import android.content.Context import android.os.Bundle import android.os.PowerManager import android.os.PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.activities.GenericActivity import org.linphone.core.tools.Log abstract class ProximitySensorActivity : GenericActivity() { @@ -49,7 +48,7 @@ abstract class ProximitySensorActivity : GenericActivity() { super.onResume() if (coreContext.core.callsNb > 0) { - val videoEnabled = coreContext.isVideoCallOrConferenceActive() + val videoEnabled = coreContext.core.currentCall?.currentParams?.isVideoEnabled ?: false enableProximitySensor(!videoEnabled) } } diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt index 1f7fa1f26..66d1ed98a 100644 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt @@ -22,7 +22,7 @@ package org.linphone.activities.assistant.fragments import android.os.Bundle import android.view.View import androidx.lifecycle.ViewModelProvider -import org.linphone.LinphoneApplication +import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R import org.linphone.activities.assistant.AssistantActivity import org.linphone.activities.assistant.viewmodels.* @@ -87,7 +87,7 @@ class PhoneAccountLinkingFragment : AbstractPhoneFragment. - */ -package org.linphone.activities.call - -import android.content.Intent -import android.content.res.Configuration -import android.content.res.Resources -import android.os.Bundle -import android.view.Gravity -import android.view.View -import androidx.constraintlayout.widget.ConstraintSet -import androidx.databinding.DataBindingUtil -import androidx.lifecycle.ViewModelProvider -import androidx.window.layout.FoldingFeature -import kotlinx.coroutines.* -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.call.viewmodels.* -import org.linphone.activities.main.MainActivity -import org.linphone.compatibility.Compatibility -import org.linphone.core.tools.Log -import org.linphone.databinding.CallActivityBinding - -class CallActivity : ProximitySensorActivity() { - private lateinit var binding: CallActivityBinding - private lateinit var viewModel: ControlsFadingViewModel - private lateinit var sharedViewModel: SharedCallViewModel - - private var foldingFeature: FoldingFeature? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - Compatibility.setShowWhenLocked(this, true) - Compatibility.setTurnScreenOn(this, true) - - binding = DataBindingUtil.setContentView(this, R.layout.call_activity) - binding.lifecycleOwner = this - - viewModel = ViewModelProvider(this)[ControlsFadingViewModel::class.java] - binding.controlsFadingViewModel = viewModel - - sharedViewModel = ViewModelProvider(this)[SharedCallViewModel::class.java] - - sharedViewModel.toggleDrawerEvent.observe( - this - ) { - it.consume { - if (binding.statsMenu.isDrawerOpen(Gravity.LEFT)) { - binding.statsMenu.closeDrawer(binding.sideMenuContent, true) - } else { - binding.statsMenu.openDrawer(binding.sideMenuContent, true) - } - } - } - - sharedViewModel.resetHiddenInterfaceTimerInVideoCallEvent.observe( - this - ) { - it.consume { - viewModel.showMomentarily() - } - } - - viewModel.proximitySensorEnabled.observe( - this - ) { - enableProximitySensor(it) - } - - viewModel.videoEnabled.observe( - this - ) { - updateConstraintSetDependingOnFoldingState() - } - } - - override fun onLayoutChanges(foldingFeature: FoldingFeature?) { - this.foldingFeature = foldingFeature - updateConstraintSetDependingOnFoldingState() - } - - override fun onResume() { - super.onResume() - - if (coreContext.core.callsNb == 0) { - Log.w("[Call Activity] Resuming but no call found...") - if (isTaskRoot) { - // When resuming app from recent tasks make sure MainActivity will be launched if there is no call - val intent = Intent() - intent.setClass(this, MainActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - startActivity(intent) - } else { - finish() - } - } else { - coreContext.removeCallOverlay() - } - - if (corePreferences.fullScreenCallUI) { - hideSystemUI() - window.decorView.setOnSystemUiVisibilityChangeListener { visibility -> - if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) { - GlobalScope.launch { - delay(2000) - withContext(Dispatchers.Main) { - hideSystemUI() - } - } - } - } - } - } - - override fun onPause() { - val core = coreContext.core - if (core.callsNb > 0) { - coreContext.createCallOverlay() - } - - super.onPause() - } - - override fun onDestroy() { - coreContext.core.nativeVideoWindowId = null - coreContext.core.nativePreviewWindowId = null - - super.onDestroy() - } - - override fun onUserLeaveHint() { - super.onUserLeaveHint() - - if (coreContext.isVideoCallOrConferenceActive()) { - Compatibility.enterPipMode(this) - } - } - - override fun onPictureInPictureModeChanged( - isInPictureInPictureMode: Boolean, - newConfig: Configuration - ) { - if (isInPictureInPictureMode) { - viewModel.areControlsHidden.value = true - } - - if (corePreferences.hideCameraPreviewInPipMode) { - viewModel.isVideoPreviewHidden.value = isInPictureInPictureMode - } else { - viewModel.isVideoPreviewResizedForPip.value = isInPictureInPictureMode - } - } - - override fun getTheme(): Resources.Theme { - val theme = super.getTheme() - if (corePreferences.fullScreenCallUI) { - theme.applyStyle(R.style.FullScreenTheme, true) - } - return theme - } - - private fun hideSystemUI() { - window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or - View.SYSTEM_UI_FLAG_IMMERSIVE or - View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or - View.SYSTEM_UI_FLAG_LAYOUT_STABLE or - View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION - } - - private fun updateConstraintSetDependingOnFoldingState() { - val feature = foldingFeature ?: return - val constraintLayout = binding.constraintLayout - val set = ConstraintSet() - set.clone(constraintLayout) - - if (feature.state == FoldingFeature.State.HALF_OPENED && viewModel.videoEnabled.value == true) { - set.setGuidelinePercent(R.id.hinge_top, 0.5f) - set.setGuidelinePercent(R.id.hinge_bottom, 0.5f) - viewModel.disable(true) - } else { - set.setGuidelinePercent(R.id.hinge_top, 0f) - set.setGuidelinePercent(R.id.hinge_bottom, 1f) - viewModel.disable(false) - } - - set.applyTo(constraintLayout) - } -} diff --git a/app/src/main/java/org/linphone/activities/call/IncomingCallActivity.kt b/app/src/main/java/org/linphone/activities/call/IncomingCallActivity.kt deleted file mode 100644 index c150de7a0..000000000 --- a/app/src/main/java/org/linphone/activities/call/IncomingCallActivity.kt +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.call - -import android.Manifest -import android.annotation.TargetApi -import android.app.KeyguardManager -import android.content.Context -import android.content.Intent -import android.content.pm.ActivityInfo -import android.content.pm.PackageManager -import android.os.Bundle -import androidx.databinding.DataBindingUtil -import androidx.lifecycle.ViewModelProvider -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.activities.GenericActivity -import org.linphone.activities.call.viewmodels.IncomingCallViewModel -import org.linphone.activities.call.viewmodels.IncomingCallViewModelFactory -import org.linphone.activities.main.MainActivity -import org.linphone.compatibility.Compatibility -import org.linphone.core.Call -import org.linphone.core.tools.Log -import org.linphone.databinding.CallIncomingActivityBinding -import org.linphone.mediastream.Version -import org.linphone.utils.PermissionHelper - -class IncomingCallActivity : GenericActivity() { - private lateinit var binding: CallIncomingActivityBinding - private lateinit var viewModel: IncomingCallViewModel - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - Compatibility.setShowWhenLocked(this, true) - Compatibility.setTurnScreenOn(this, true) - // Leaks on API 27+: https://stackoverflow.com/questions/60477120/keyguardmanager-memory-leak - Compatibility.requestDismissKeyguard(this) - - binding = DataBindingUtil.setContentView(this, R.layout.call_incoming_activity) - binding.lifecycleOwner = this - - val incomingCall: Call? = findIncomingCall() - if (incomingCall == null) { - Log.e("[Incoming Call Activity] Couldn't find call in state Incoming") - if (isTaskRoot) { - // When resuming app from recent tasks make sure MainActivity will be launched if there is no call - val intent = Intent() - intent.setClass(this, MainActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - startActivity(intent) - } else { - finish() - } - return - } - - viewModel = ViewModelProvider( - this, - IncomingCallViewModelFactory(incomingCall) - )[IncomingCallViewModel::class.java] - binding.viewModel = viewModel - - viewModel.callEndedEvent.observe( - this - ) { - it.consume { - Log.i("[Incoming Call Activity] Call ended, finish activity") - finish() - } - } - - viewModel.earlyMediaVideoEnabled.observe( - this - ) { - if (it) { - Log.i("[Incoming Call Activity] Early media video being received, set native window id") - coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface - } - } - - val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager - val keyguardLocked = keyguardManager.isKeyguardLocked - viewModel.screenLocked.value = keyguardLocked - if (keyguardLocked) { - // Forbid screen rotation to prevent keyguard to show up above incoming call view - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED - } - - binding.buttons.setViewModel(viewModel) - - if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) { - checkPermissions() - } - } - - override fun onResume() { - super.onResume() - - val incomingCall: Call? = findIncomingCall() - if (incomingCall == null) { - Log.e("[Incoming Call Activity] Couldn't find call in state Incoming") - if (isTaskRoot) { - // When resuming app from recent tasks make sure MainActivity will be launched if there is no call - val intent = Intent() - intent.setClass(this, MainActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - startActivity(intent) - } else { - finish() - } - } - } - - @TargetApi(Version.API23_MARSHMALLOW_60) - private fun checkPermissions() { - val permissionsRequiredList = arrayListOf() - if (!PermissionHelper.get().hasRecordAudioPermission()) { - Log.i("[Incoming Call Activity] Asking for RECORD_AUDIO permission") - permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO) - } - - if (viewModel.call.currentParams.isVideoEnabled && !PermissionHelper.get().hasCameraPermission()) { - Log.i("[Incoming Call Activity] Asking for CAMERA permission") - permissionsRequiredList.add(Manifest.permission.CAMERA) - } - - if (permissionsRequiredList.isNotEmpty()) { - val permissionsRequired = arrayOfNulls(permissionsRequiredList.size) - permissionsRequiredList.toArray(permissionsRequired) - requestPermissions(permissionsRequired, 0) - } - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - if (requestCode == 0) { - for (i in permissions.indices) { - when (permissions[i]) { - Manifest.permission.RECORD_AUDIO -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { - Log.i("[Incoming Call Activity] RECORD_AUDIO permission has been granted") - } - Manifest.permission.CAMERA -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { - Log.i("[Incoming Call Activity] CAMERA permission has been granted") - coreContext.core.reloadVideoDevices() - } - } - } - } - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - } - - private fun findIncomingCall(): Call? { - for (call in coreContext.core.calls) { - if (call.state == Call.State.IncomingReceived || - call.state == Call.State.IncomingEarlyMedia - ) { - return call - } - } - return null - } -} diff --git a/app/src/main/java/org/linphone/activities/call/OutgoingCallActivity.kt b/app/src/main/java/org/linphone/activities/call/OutgoingCallActivity.kt deleted file mode 100644 index 5dc2a1676..000000000 --- a/app/src/main/java/org/linphone/activities/call/OutgoingCallActivity.kt +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.call - -import android.Manifest -import android.animation.ValueAnimator -import android.annotation.TargetApi -import android.content.Intent -import android.content.pm.PackageManager -import android.os.Bundle -import androidx.databinding.DataBindingUtil -import androidx.lifecycle.ViewModelProvider -import com.google.android.flexbox.FlexboxLayout -import org.linphone.LinphoneApplication -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.activities.call.viewmodels.CallViewModel -import org.linphone.activities.call.viewmodels.CallViewModelFactory -import org.linphone.activities.call.viewmodels.ControlsViewModel -import org.linphone.activities.main.MainActivity -import org.linphone.core.Call -import org.linphone.core.tools.Log -import org.linphone.databinding.CallOutgoingActivityBinding -import org.linphone.mediastream.Version -import org.linphone.utils.PermissionHelper - -class OutgoingCallActivity : ProximitySensorActivity() { - private lateinit var binding: CallOutgoingActivityBinding - private lateinit var viewModel: CallViewModel - private lateinit var controlsViewModel: ControlsViewModel - - // We have to use lateinit here because we need to compute the screen width first - private lateinit var numpadAnimator: ValueAnimator - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - binding = DataBindingUtil.setContentView(this, R.layout.call_outgoing_activity) - binding.lifecycleOwner = this - - val outgoingCall: Call? = findOutgoingCall() - if (outgoingCall == null) { - Log.e("[Outgoing Call Activity] Couldn't find call in state Outgoing") - if (isTaskRoot) { - // When resuming app from recent tasks make sure MainActivity will be launched if there is no call - val intent = Intent() - intent.setClass(this, MainActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - startActivity(intent) - } else { - finish() - } - return - } - - viewModel = ViewModelProvider( - this, - CallViewModelFactory(outgoingCall) - )[CallViewModel::class.java] - binding.viewModel = viewModel - - controlsViewModel = ViewModelProvider(this)[ControlsViewModel::class.java] - binding.controlsViewModel = controlsViewModel - - viewModel.callEndedEvent.observe( - this - ) { - it.consume { - Log.i("[Outgoing Call Activity] Call ended, finish activity") - finish() - } - } - - viewModel.callConnectedEvent.observe( - this - ) { - it.consume { - Log.i("[Outgoing Call Activity] Call connected, finish activity") - finish() - } - } - - controlsViewModel.isSpeakerSelected.observe( - this - ) { - enableProximitySensor(!it) - } - - controlsViewModel.askAudioRecordPermissionEvent.observe( - this - ) { - it.consume { permission -> - requestPermissions(arrayOf(permission), 0) - } - } - - controlsViewModel.askCameraPermissionEvent.observe( - this - ) { - it.consume { permission -> - requestPermissions(arrayOf(permission), 0) - } - } - - controlsViewModel.toggleNumpadEvent.observe( - this - ) { - it.consume { open -> - if (this::numpadAnimator.isInitialized) { - if (open) { - numpadAnimator.start() - } else { - numpadAnimator.reverse() - } - } - } - } - - if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) { - checkPermissions() - } - } - - override fun onStart() { - super.onStart() - initNumpadLayout() - } - - override fun onStop() { - numpadAnimator.end() - super.onStop() - } - - override fun onResume() { - super.onResume() - - val outgoingCall: Call? = findOutgoingCall() - if (outgoingCall == null) { - Log.e("[Outgoing Call Activity] Couldn't find call in state Outgoing") - if (isTaskRoot) { - // When resuming app from recent tasks make sure MainActivity will be launched if there is no call - val intent = Intent() - intent.setClass(this, MainActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - startActivity(intent) - } else { - finish() - } - } - } - - @TargetApi(Version.API23_MARSHMALLOW_60) - private fun checkPermissions() { - val permissionsRequiredList = arrayListOf() - if (!PermissionHelper.get().hasRecordAudioPermission()) { - Log.i("[Outgoing Call Activity] Asking for RECORD_AUDIO permission") - permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO) - } - if (viewModel.call.currentParams.isVideoEnabled && !PermissionHelper.get().hasCameraPermission()) { - Log.i("[Outgoing Call Activity] Asking for CAMERA permission") - permissionsRequiredList.add(Manifest.permission.CAMERA) - } - if (permissionsRequiredList.isNotEmpty()) { - val permissionsRequired = arrayOfNulls(permissionsRequiredList.size) - permissionsRequiredList.toArray(permissionsRequired) - requestPermissions(permissionsRequired, 0) - } - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - if (requestCode == 0) { - for (i in permissions.indices) { - when (permissions[i]) { - Manifest.permission.RECORD_AUDIO -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { - Log.i("[Outgoing Call Activity] RECORD_AUDIO permission has been granted") - controlsViewModel.updateMuteMicState() - } - Manifest.permission.CAMERA -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { - Log.i("[Outgoing Call Activity] CAMERA permission has been granted") - coreContext.core.reloadVideoDevices() - } - } - } - } - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - } - - private fun findOutgoingCall(): Call? { - for (call in coreContext.core.calls) { - if (call.state == Call.State.OutgoingInit || - call.state == Call.State.OutgoingProgress || - call.state == Call.State.OutgoingRinging || - call.state == Call.State.OutgoingEarlyMedia - ) { - return call - } - } - return null - } - - private fun initNumpadLayout() { - val screenWidth = coreContext.screenWidth - numpadAnimator = ValueAnimator.ofFloat(screenWidth, 0f).apply { - addUpdateListener { - val value = it.animatedValue as Float - findViewById(R.id.numpad)?.translationX = -value - duration = if (LinphoneApplication.corePreferences.enableAnimations) 500 else 0 - } - } - // Hide the numpad here as we can't set the translationX property on include tag in layout - if (this::controlsViewModel.isInitialized && controlsViewModel.numpadVisibility.value == false) { - findViewById(R.id.numpad)?.translationX = -screenWidth - } - } -} diff --git a/app/src/main/java/org/linphone/activities/call/VideoZoomHelper.kt b/app/src/main/java/org/linphone/activities/call/VideoZoomHelper.kt deleted file mode 100644 index 820bd3e95..000000000 --- a/app/src/main/java/org/linphone/activities/call/VideoZoomHelper.kt +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.call - -import android.content.Context -import android.view.GestureDetector -import android.view.MotionEvent -import android.view.ScaleGestureDetector -import android.view.View -import kotlin.math.max -import kotlin.math.min -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.core.Call - -class VideoZoomHelper(context: Context, private var videoDisplayView: View) : GestureDetector.SimpleOnGestureListener() { - private var scaleDetector: ScaleGestureDetector - - private var zoomFactor = 1f - private var zoomCenterX = 0f - private var zoomCenterY = 0f - - init { - val gestureDetector = GestureDetector(context, this) - - scaleDetector = ScaleGestureDetector( - context, - object : - ScaleGestureDetector.SimpleOnScaleGestureListener() { - override fun onScale(detector: ScaleGestureDetector): Boolean { - zoomFactor *= detector.scaleFactor - // Don't let the object get too small or too large. - // Zoom to make the video fill the screen vertically - val portraitZoomFactor = videoDisplayView.height.toFloat() / (3 * videoDisplayView.width / 4) - // Zoom to make the video fill the screen horizontally - val landscapeZoomFactor = videoDisplayView.width.toFloat() / (3 * videoDisplayView.height / 4) - zoomFactor = max(0.1f, min(zoomFactor, max(portraitZoomFactor, landscapeZoomFactor))) - - val currentCall: Call? = coreContext.core.currentCall - if (currentCall != null) { - currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY) - return true - } - - return false - } - } - ) - - videoDisplayView.setOnTouchListener { _, event -> - val currentZoomFactor = zoomFactor - scaleDetector.onTouchEvent(event) - - if (currentZoomFactor != zoomFactor) { - // We did scale, prevent touch event from going further - return@setOnTouchListener true - } - - // If true, gesture detected, prevent touch event from going further - // Otherwise it seems we didn't use event, - // allow it to be dispatched somewhere else - gestureDetector.onTouchEvent(event) - } - } - - override fun onScroll( - e1: MotionEvent, - e2: MotionEvent, - distanceX: Float, - distanceY: Float - ): Boolean { - val currentCall: Call? = coreContext.core.currentCall - if (currentCall != null) { - if (zoomFactor > 1) { - // Video is zoomed, slide is used to change center of zoom - if (distanceX > 0 && zoomCenterX < 1) { - zoomCenterX += 0.01f - } else if (distanceX < 0 && zoomCenterX > 0) { - zoomCenterX -= 0.01f - } - - if (distanceY < 0 && zoomCenterY < 1) { - zoomCenterY += 0.01f - } else if (distanceY > 0 && zoomCenterY > 0) { - zoomCenterY -= 0.01f - } - - if (zoomCenterX > 1) zoomCenterX = 1f - if (zoomCenterX < 0) zoomCenterX = 0f - if (zoomCenterY > 1) zoomCenterY = 1f - if (zoomCenterY < 0) zoomCenterY = 0f - - currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY) - return true - } - } - - return false - } - - override fun onDoubleTap(e: MotionEvent?): Boolean { - val currentCall: Call? = coreContext.core.currentCall - if (currentCall != null) { - if (zoomFactor == 1f) { - // Zoom to make the video fill the screen vertically - val portraitZoomFactor = videoDisplayView.height.toFloat() / (3 * videoDisplayView.width / 4) - // Zoom to make the video fill the screen horizontally - val landscapeZoomFactor = videoDisplayView.width.toFloat() / (3 * videoDisplayView.height / 4) - zoomFactor = max(portraitZoomFactor, landscapeZoomFactor) - } else { - resetZoom() - } - - currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY) - return true - } - - return false - } - - private fun resetZoom() { - zoomFactor = 1f - zoomCenterY = 0.5f - zoomCenterX = zoomCenterY - } -} diff --git a/app/src/main/java/org/linphone/activities/call/data/ConferenceParticipantData.kt b/app/src/main/java/org/linphone/activities/call/data/ConferenceParticipantData.kt deleted file mode 100644 index 2af5a7a8a..000000000 --- a/app/src/main/java/org/linphone/activities/call/data/ConferenceParticipantData.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.call.data - -import androidx.lifecycle.MutableLiveData -import org.linphone.contact.GenericContactData -import org.linphone.core.Conference -import org.linphone.core.Participant -import org.linphone.core.tools.Log - -class ConferenceParticipantData( - private val conference: Conference, - val participant: Participant -) : - GenericContactData(participant.address) { - private val isAdmin = MutableLiveData() - val isMeAdmin = MutableLiveData() - - init { - isAdmin.value = participant.isAdmin - isMeAdmin.value = conference.me.isAdmin - Log.i("[Conference Participant VM] Participant ${participant.address.asStringUriOnly()} is ${if (participant.isAdmin) "admin" else "not admin"}") - Log.i("[Conference Participant VM] Me is ${if (conference.me.isAdmin) "admin" else "not admin"} and is ${if (conference.me.isFocus) "focus" else "not focus"}") - } - - fun removeFromConference() { - Log.i("[Conference Participant VM] Removing participant ${participant.address.asStringUriOnly()} from conference $conference") - conference.removeParticipant(participant) - } -} diff --git a/app/src/main/java/org/linphone/activities/call/fragments/ControlsFragment.kt b/app/src/main/java/org/linphone/activities/call/fragments/ControlsFragment.kt deleted file mode 100644 index 3765a4183..000000000 --- a/app/src/main/java/org/linphone/activities/call/fragments/ControlsFragment.kt +++ /dev/null @@ -1,323 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.call.fragments - -import android.Manifest -import android.animation.ValueAnimator -import android.annotation.TargetApi -import android.app.Dialog -import android.content.Intent -import android.content.pm.PackageManager.PERMISSION_GRANTED -import android.os.Bundle -import android.os.SystemClock -import android.view.View -import androidx.lifecycle.ViewModelProvider -import com.google.android.flexbox.FlexboxLayout -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.GenericFragment -import org.linphone.activities.call.viewmodels.CallsViewModel -import org.linphone.activities.call.viewmodels.ConferenceViewModel -import org.linphone.activities.call.viewmodels.ControlsViewModel -import org.linphone.activities.call.viewmodels.SharedCallViewModel -import org.linphone.activities.main.MainActivity -import org.linphone.activities.main.viewmodels.DialogViewModel -import org.linphone.core.Call -import org.linphone.core.tools.Log -import org.linphone.databinding.CallControlsFragmentBinding -import org.linphone.mediastream.Version -import org.linphone.utils.AppUtils -import org.linphone.utils.DialogUtils -import org.linphone.utils.Event -import org.linphone.utils.PermissionHelper - -class ControlsFragment : GenericFragment() { - private lateinit var callsViewModel: CallsViewModel - private lateinit var controlsViewModel: ControlsViewModel - private lateinit var conferenceViewModel: ConferenceViewModel - private lateinit var sharedViewModel: SharedCallViewModel - - private var dialog: Dialog? = null - - override fun getLayoutId(): Int = R.layout.call_controls_fragment - - // We have to use lateinit here because we need to compute the screen width first - private lateinit var numpadAnimator: ValueAnimator - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - useMaterialSharedAxisXForwardAnimation = false - - sharedViewModel = requireActivity().run { - ViewModelProvider(this)[SharedCallViewModel::class.java] - } - - callsViewModel = requireActivity().run { - ViewModelProvider(this)[CallsViewModel::class.java] - } - binding.viewModel = callsViewModel - - controlsViewModel = requireActivity().run { - ViewModelProvider(this)[ControlsViewModel::class.java] - } - binding.controlsViewModel = controlsViewModel - - conferenceViewModel = requireActivity().run { - ViewModelProvider(this)[ConferenceViewModel::class.java] - } - binding.conferenceViewModel = conferenceViewModel - - callsViewModel.currentCallViewModel.observe( - viewLifecycleOwner - ) { - if (it != null) { - binding.activeCallTimer.base = - SystemClock.elapsedRealtime() - (1000 * it.call.duration) // Linphone timestamps are in seconds - binding.activeCallTimer.start() - } - } - - callsViewModel.noMoreCallEvent.observe( - viewLifecycleOwner - ) { - it.consume { - requireActivity().finish() - } - } - - callsViewModel.askWriteExternalStoragePermissionEvent.observe( - viewLifecycleOwner - ) { - it.consume { - if (!PermissionHelper.get().hasWriteExternalStoragePermission()) { - Log.i("[Controls Fragment] Asking for WRITE_EXTERNAL_STORAGE permission") - requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 2) - } - } - } - - callsViewModel.callUpdateEvent.observe( - viewLifecycleOwner - ) { - it.consume { call -> - if (call.state == Call.State.StreamsRunning) { - dialog?.dismiss() - } else if (call.state == Call.State.UpdatedByRemote) { - if (coreContext.core.isVideoCaptureEnabled || coreContext.core.isVideoDisplayEnabled) { - if (call.currentParams.isVideoEnabled != call.remoteParams?.isVideoEnabled) { - showCallVideoUpdateDialog(call) - } - } else { - Log.w("[Controls Fragment] Video display & capture are disabled, don't show video dialog") - } - } - } - } - - controlsViewModel.chatClickedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - val intent = Intent() - intent.setClass(requireContext(), MainActivity::class.java) - intent.putExtra("Chat", true) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - } - } - - controlsViewModel.addCallClickedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - val intent = Intent() - intent.setClass(requireContext(), MainActivity::class.java) - intent.putExtra("Dialer", true) - intent.putExtra("Transfer", false) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - } - } - - controlsViewModel.transferCallClickedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - val intent = Intent() - intent.setClass(requireContext(), MainActivity::class.java) - intent.putExtra("Dialer", true) - intent.putExtra("Transfer", true) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - } - } - - controlsViewModel.askAudioRecordPermissionEvent.observe( - viewLifecycleOwner - ) { - it.consume { permission -> - Log.i("[Controls Fragment] Asking for $permission permission") - requestPermissions(arrayOf(permission), 0) - } - } - - controlsViewModel.askCameraPermissionEvent.observe( - viewLifecycleOwner - ) { - it.consume { permission -> - Log.i("[Controls Fragment] Asking for $permission permission") - requestPermissions(arrayOf(permission), 1) - } - } - - controlsViewModel.toggleNumpadEvent.observe( - viewLifecycleOwner - ) { - it.consume { open -> - if (this::numpadAnimator.isInitialized) { - if (open) { - numpadAnimator.start() - } else { - numpadAnimator.reverse() - } - } - } - } - - controlsViewModel.somethingClickedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - sharedViewModel.resetHiddenInterfaceTimerInVideoCallEvent.value = Event(true) - } - } - - if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) { - checkPermissions() - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - onBackPressedCallback.isEnabled = false - } - - override fun onStart() { - super.onStart() - initNumpadLayout() - } - - override fun onStop() { - numpadAnimator.end() - super.onStop() - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - if (requestCode == 0) { - for (i in permissions.indices) { - when (permissions[i]) { - Manifest.permission.RECORD_AUDIO -> if (grantResults[i] == PERMISSION_GRANTED) { - Log.i("[Controls Fragment] RECORD_AUDIO permission has been granted") - controlsViewModel.updateMuteMicState() - } - Manifest.permission.CAMERA -> if (grantResults[i] == PERMISSION_GRANTED) { - Log.i("[Controls Fragment] CAMERA permission has been granted") - coreContext.core.reloadVideoDevices() - } - } - } - } else if (requestCode == 1) { - if (grantResults.isNotEmpty() && grantResults[0] == PERMISSION_GRANTED) { - Log.i("[Controls Fragment] CAMERA permission has been granted") - coreContext.core.reloadVideoDevices() - controlsViewModel.toggleVideo() - } - } else if (requestCode == 2 && grantResults.isNotEmpty() && grantResults[0] == PERMISSION_GRANTED) { - callsViewModel.takeScreenshot() - } - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - } - - @TargetApi(Version.API23_MARSHMALLOW_60) - private fun checkPermissions() { - val permissionsRequiredList = arrayListOf() - - if (!PermissionHelper.get().hasRecordAudioPermission()) { - Log.i("[Controls Fragment] Asking for RECORD_AUDIO permission") - permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO) - } - - if (coreContext.isVideoCallOrConferenceActive() && !PermissionHelper.get().hasCameraPermission()) { - Log.i("[Controls Fragment] Asking for CAMERA permission") - permissionsRequiredList.add(Manifest.permission.CAMERA) - } - - if (permissionsRequiredList.isNotEmpty()) { - val permissionsRequired = arrayOfNulls(permissionsRequiredList.size) - permissionsRequiredList.toArray(permissionsRequired) - requestPermissions(permissionsRequired, 0) - } - } - - private fun showCallVideoUpdateDialog(call: Call) { - val viewModel = DialogViewModel(AppUtils.getString(R.string.call_video_update_requested_dialog)) - dialog = DialogUtils.getDialog(requireContext(), viewModel) - - viewModel.showCancelButton( - { - callsViewModel.answerCallVideoUpdateRequest(call, false) - dialog?.dismiss() - }, - getString(R.string.dialog_decline) - ) - - viewModel.showOkButton( - { - callsViewModel.answerCallVideoUpdateRequest(call, true) - dialog?.dismiss() - }, - getString(R.string.dialog_accept) - ) - - dialog?.show() - } - - private fun initNumpadLayout() { - val screenWidth = coreContext.screenWidth - numpadAnimator = ValueAnimator.ofFloat(screenWidth, 0f).apply { - addUpdateListener { - val value = it.animatedValue as Float - view?.findViewById(R.id.numpad)?.translationX = -value - duration = if (corePreferences.enableAnimations) 500 else 0 - } - } - // Hide the numpad here as we can't set the translationX property on include tag in layout - if (controlsViewModel.numpadVisibility.value == false) { - view?.findViewById(R.id.numpad)?.translationX = -screenWidth - } - } -} diff --git a/app/src/main/java/org/linphone/activities/call/fragments/VideoRenderingFragment.kt b/app/src/main/java/org/linphone/activities/call/fragments/VideoRenderingFragment.kt deleted file mode 100644 index c7f980c52..000000000 --- a/app/src/main/java/org/linphone/activities/call/fragments/VideoRenderingFragment.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2010-2021 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.call.fragments - -import android.os.Bundle -import android.view.MotionEvent -import android.view.View -import androidx.lifecycle.ViewModelProvider -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.activities.GenericFragment -import org.linphone.activities.call.VideoZoomHelper -import org.linphone.activities.call.viewmodels.CallsViewModel -import org.linphone.activities.call.viewmodels.ConferenceViewModel -import org.linphone.activities.call.viewmodels.ControlsFadingViewModel -import org.linphone.databinding.CallVideoFragmentBinding - -class VideoRenderingFragment : GenericFragment() { - private lateinit var controlsFadingViewModel: ControlsFadingViewModel - private lateinit var callsViewModel: CallsViewModel - private lateinit var conferenceViewModel: ConferenceViewModel - - private var previewX: Float = 0f - private var previewY: Float = 0f - private lateinit var videoZoomHelper: VideoZoomHelper - - override fun getLayoutId(): Int = R.layout.call_video_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = this - - controlsFadingViewModel = requireActivity().run { - ViewModelProvider(this)[ControlsFadingViewModel::class.java] - } - binding.controlsFadingViewModel = controlsFadingViewModel - - callsViewModel = requireActivity().run { - ViewModelProvider(this)[CallsViewModel::class.java] - } - - conferenceViewModel = requireActivity().run { - ViewModelProvider(this)[ConferenceViewModel::class.java] - } - binding.conferenceViewModel = conferenceViewModel - - coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface - coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface - - binding.setPreviewTouchListener { v, event -> - when (event.action) { - MotionEvent.ACTION_DOWN -> { - previewX = v.x - event.rawX - previewY = v.y - event.rawY - } - MotionEvent.ACTION_MOVE -> { - v.animate() - .x(event.rawX + previewX) - .y(event.rawY + previewY) - .setDuration(0) - .start() - } - else -> { - v.performClick() - false - } - } - true - } - - videoZoomHelper = VideoZoomHelper(requireContext(), binding.remoteVideoSurface) - } -} diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/CallViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/CallViewModel.kt deleted file mode 100644 index 59216ead9..000000000 --- a/app/src/main/java/org/linphone/activities/call/viewmodels/CallViewModel.kt +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.call.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import java.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.compatibility.Compatibility -import org.linphone.contact.GenericContactViewModel -import org.linphone.core.Call -import org.linphone.core.CallListenerStub -import org.linphone.core.Factory -import org.linphone.core.tools.Log -import org.linphone.utils.Event -import org.linphone.utils.FileUtils -import org.linphone.utils.LinphoneUtils - -class CallViewModelFactory(private val call: Call) : - ViewModelProvider.NewInstanceFactory() { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return CallViewModel(call) as T - } -} - -open class CallViewModel(val call: Call) : GenericContactViewModel(call.remoteAddress) { - val address: String by lazy { - LinphoneUtils.getDisplayableAddress(call.remoteAddress) - } - - val isPaused = MutableLiveData() - - val isOutgoingEarlyMedia = MutableLiveData() - - val callEndedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val callConnectedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - private var timer: Timer? = null - - private val listener = object : CallListenerStub() { - override fun onStateChanged(call: Call, state: Call.State, message: String) { - if (call != this@CallViewModel.call) return - - isPaused.value = state == Call.State.Paused - isOutgoingEarlyMedia.value = state == Call.State.OutgoingEarlyMedia - - if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) { - timer?.cancel() - callEndedEvent.value = Event(true) - - if (state == Call.State.Error) { - Log.e("[Call View Model] Error state reason is ${call.reason}") - } - } else if (call.state == Call.State.Connected) { - callConnectedEvent.value = Event(true) - } else if (call.state == Call.State.StreamsRunning) { - // Stop call update timer once user has accepted or declined call update - timer?.cancel() - } else if (call.state == Call.State.UpdatedByRemote) { - // User has 30 secs to accept or decline call update - // Dialog to accept or decline is handled by CallsViewModel & ControlsFragment - startTimer(call) - } - } - - override fun onSnapshotTaken(call: Call, filePath: String) { - Log.i("[Call View Model] Snapshot taken, saved at $filePath") - val content = Factory.instance().createContent() - content.filePath = filePath - content.type = "image" - content.subtype = "jpeg" - content.name = filePath.substring(filePath.indexOf("/") + 1) - - viewModelScope.launch { - if (Compatibility.addImageToMediaStore(coreContext.context, content)) { - Log.i("[Call View Model] Adding snapshot ${content.name} to Media Store terminated") - } else { - Log.e("[Call View Model] Something went wrong while copying file to Media Store...") - } - } - } - } - - init { - call.addListener(listener) - - isPaused.value = call.state == Call.State.Paused - isOutgoingEarlyMedia.value = call.state == Call.State.OutgoingEarlyMedia - } - - override fun onCleared() { - destroy() - super.onCleared() - } - - fun destroy() { - call.removeListener(listener) - } - - fun terminateCall() { - coreContext.terminateCall(call) - } - - fun pause() { - call.pause() - } - - fun resume() { - call.resume() - } - - fun takeScreenshot() { - if (call.currentParams.isVideoEnabled) { - val fileName = System.currentTimeMillis().toString() + ".jpeg" - call.takeVideoSnapshot(FileUtils.getFileStoragePath(fileName).absolutePath) - } - } - - private fun startTimer(call: Call) { - timer?.cancel() - - timer = Timer("Call update timeout") - timer?.schedule( - object : TimerTask() { - override fun run() { - // Decline call update - viewModelScope.launch { - withContext(Dispatchers.Main) { - coreContext.answerCallVideoUpdateRequest(call, false) - } - } - } - }, - 30000 - ) - } -} diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/CallsViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/CallsViewModel.kt deleted file mode 100644 index e6165e651..000000000 --- a/app/src/main/java/org/linphone/activities/call/viewmodels/CallsViewModel.kt +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.call.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.core.* -import org.linphone.core.tools.Log -import org.linphone.utils.Event -import org.linphone.utils.PermissionHelper - -class CallsViewModel : ViewModel() { - val currentCallViewModel = MutableLiveData() - - val noActiveCall = MutableLiveData() - - val callPausedByRemote = MutableLiveData() - - val pausedCalls = MutableLiveData>() - - val noMoreCallEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val callUpdateEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val askWriteExternalStoragePermissionEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - private val listener = object : CoreListenerStub() { - override fun onCallStateChanged(core: Core, call: Call, state: Call.State, message: String) { - Log.i("[Calls VM] Call state changed: $state") - callPausedByRemote.value = (state == Call.State.PausedByRemote) and (call.conference == null) - - val currentCall = core.currentCall - noActiveCall.value = currentCall == null - if (currentCall == null) { - currentCallViewModel.value?.destroy() - } else if (currentCallViewModel.value?.call != currentCall) { - val viewModel = CallViewModel(currentCall) - currentCallViewModel.value = viewModel - } - - if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) { - if (core.callsNb == 0) { - noMoreCallEvent.value = Event(true) - } else { - removeCallFromPausedListIfPresent(call) - } - } else if (state == Call.State.Paused) { - addCallToPausedList(call) - } else if (state == Call.State.Resuming) { - removeCallFromPausedListIfPresent(call) - } else if (call.state == Call.State.UpdatedByRemote) { - // If the correspondent asks to turn on video while audio call, - // defer update until user has chosen whether to accept it or not - val remoteVideo = call.remoteParams?.isVideoEnabled ?: false - val localVideo = call.currentParams.isVideoEnabled - val autoAccept = call.core.videoActivationPolicy.automaticallyAccept - if (remoteVideo && !localVideo && !autoAccept) { - if (coreContext.core.isVideoCaptureEnabled || coreContext.core.isVideoDisplayEnabled) { - call.deferUpdate() - callUpdateEvent.value = Event(call) - } else { - coreContext.answerCallVideoUpdateRequest(call, false) - } - } - } else if (state == Call.State.StreamsRunning) { - callUpdateEvent.value = Event(call) - } - } - } - - init { - coreContext.core.addListener(listener) - - val currentCall = coreContext.core.currentCall - noActiveCall.value = currentCall == null - if (currentCall != null) { - currentCallViewModel.value?.destroy() - - val viewModel = CallViewModel(currentCall) - currentCallViewModel.value = viewModel - } - - callPausedByRemote.value = currentCall?.state == Call.State.PausedByRemote - - for (call in coreContext.core.calls) { - if (call.state == Call.State.Paused || call.state == Call.State.Pausing) { - addCallToPausedList(call) - } - } - } - - override fun onCleared() { - coreContext.core.removeListener(listener) - - super.onCleared() - } - - fun answerCallVideoUpdateRequest(call: Call, accept: Boolean) { - coreContext.answerCallVideoUpdateRequest(call, accept) - } - - fun takeScreenshot() { - if (!PermissionHelper.get().hasWriteExternalStoragePermission()) { - askWriteExternalStoragePermissionEvent.value = Event(true) - } else { - currentCallViewModel.value?.takeScreenshot() - } - } - - private fun addCallToPausedList(call: Call) { - if (call.conference != null) return // Conference will be displayed as paused, no need to display the call as well - - val list = arrayListOf() - list.addAll(pausedCalls.value.orEmpty()) - - for (pausedCallViewModel in list) { - if (pausedCallViewModel.call == call) { - return - } - } - - val viewModel = CallViewModel(call) - list.add(viewModel) - pausedCalls.value = list - } - - private fun removeCallFromPausedListIfPresent(call: Call) { - val list = arrayListOf() - list.addAll(pausedCalls.value.orEmpty()) - - for (pausedCallViewModel in list) { - if (pausedCallViewModel.call == call) { - pausedCallViewModel.destroy() - list.remove(pausedCallViewModel) - break - } - } - - pausedCalls.value = list - } -} diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/ConferenceViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/ConferenceViewModel.kt deleted file mode 100644 index 164a067bb..000000000 --- a/app/src/main/java/org/linphone/activities/call/viewmodels/ConferenceViewModel.kt +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.call.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.activities.call.data.ConferenceParticipantData -import org.linphone.core.* -import org.linphone.core.tools.Log - -class ConferenceViewModel : ViewModel() { - val isConferencePaused = MutableLiveData() - - val isMeConferenceFocus = MutableLiveData() - - val conferenceAddress = MutableLiveData
() - - val conferenceParticipants = MutableLiveData>() - - val isInConference = MutableLiveData() - - private val conferenceListener = object : ConferenceListenerStub() { - override fun onParticipantAdded(conference: Conference, participant: Participant) { - if (conference.isMe(participant.address)) { - Log.i("[Conference VM] Entered conference") - isConferencePaused.value = false - } else { - Log.i("[Conference VM] Participant added") - updateParticipantsList(conference) - } - } - - override fun onParticipantRemoved(conference: Conference, participant: Participant) { - if (conference.isMe(participant.address)) { - Log.i("[Conference VM] Left conference") - isConferencePaused.value = true - } else { - Log.i("[Conference VM] Participant removed") - updateParticipantsList(conference) - } - } - - override fun onParticipantAdminStatusChanged( - conference: Conference, - participant: Participant - ) { - Log.i("[Conference VM] Participant admin status changed") - updateParticipantsList(conference) - } - } - - private val listener = object : CoreListenerStub() { - override fun onConferenceStateChanged( - core: Core, - conference: Conference, - state: Conference.State - ) { - Log.i("[Conference VM] Conference state changed: $state") - isConferencePaused.value = !conference.isIn - - if (state == Conference.State.Instantiated) { - conference.addListener(conferenceListener) - } else if (state == Conference.State.Created) { - updateParticipantsList(conference) - isMeConferenceFocus.value = conference.me.isFocus - conferenceAddress.value = conference.conferenceAddress - } else if (state == Conference.State.Terminated || state == Conference.State.TerminationFailed) { - isInConference.value = false - conference.removeListener(conferenceListener) - conferenceParticipants.value = arrayListOf() - } - } - } - - init { - coreContext.core.addListener(listener) - - isConferencePaused.value = coreContext.core.conference?.isIn != true - isMeConferenceFocus.value = false - conferenceParticipants.value = arrayListOf() - isInConference.value = false - - val conference = coreContext.core.conference - if (conference != null) { - conference.addListener(conferenceListener) - isMeConferenceFocus.value = conference.me.isFocus - updateParticipantsList(conference) - } - } - - override fun onCleared() { - coreContext.core.removeListener(listener) - - super.onCleared() - } - - fun pauseConference() { - val defaultProxyConfig = coreContext.core.defaultProxyConfig - val localAddress = defaultProxyConfig?.identityAddress - val participants = arrayOf
() - val remoteConference = coreContext.core.searchConference(null, localAddress, conferenceAddress.value, participants) - val localConference = coreContext.core.searchConference(null, conferenceAddress.value, conferenceAddress.value, participants) - val conference = remoteConference ?: localConference - - if (conference != null) { - Log.i("[Conference VM] Leaving conference with address ${conferenceAddress.value?.asStringUriOnly()} temporarily") - conference.leave() - } else { - Log.w("[Conference VM] Unable to find conference with address ${conferenceAddress.value?.asStringUriOnly()}") - } - } - - fun resumeConference() { - val defaultProxyConfig = coreContext.core.defaultProxyConfig - val localAddress = defaultProxyConfig?.identityAddress - val participants = arrayOf
() - val remoteConference = coreContext.core.searchConference(null, localAddress, conferenceAddress.value, participants) - val localConference = coreContext.core.searchConference(null, conferenceAddress.value, conferenceAddress.value, participants) - val conference = remoteConference ?: localConference - - if (conference != null) { - Log.i("[Conference VM] Entering again conference with address ${conferenceAddress.value?.asStringUriOnly()}") - conference.enter() - } else { - Log.w("[Conference VM] Unable to find conference with address ${conferenceAddress.value?.asStringUriOnly()}") - } - } - - private fun updateParticipantsList(conference: Conference) { - val participants = arrayListOf() - for (participant in conference.participantList) { - Log.i("[Conference VM] Participant found: ${participant.address.asStringUriOnly()}") - val viewModel = ConferenceParticipantData(conference, participant) - participants.add(viewModel) - } - conferenceParticipants.value = participants - isInConference.value = participants.isNotEmpty() - } -} diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsFadingViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsFadingViewModel.kt deleted file mode 100644 index 1d231a8db..000000000 --- a/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsFadingViewModel.kt +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.call.viewmodels - -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import java.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.core.AudioDevice -import org.linphone.core.Call -import org.linphone.core.Core -import org.linphone.core.CoreListenerStub -import org.linphone.core.tools.Log - -class ControlsFadingViewModel : ViewModel() { - val areControlsHidden = MutableLiveData() - - val isVideoPreviewHidden = MutableLiveData() - val isVideoPreviewResizedForPip = MutableLiveData() - - val videoEnabled = MutableLiveData() - - val proximitySensorEnabled: MediatorLiveData = MediatorLiveData() - - private val nonEarpieceOutputAudioDevice = MutableLiveData() - - private var timer: Timer? = null - - private var disabled: Boolean = false - - private val listener = object : CoreListenerStub() { - override fun onCallStateChanged( - core: Core, - call: Call, - state: Call.State, - message: String - ) { - if (state == Call.State.StreamsRunning || state == Call.State.Updating || state == Call.State.UpdatedByRemote) { - val isVideoCall = coreContext.isVideoCallOrConferenceActive() - Log.i("[Controls Fading] Call is in state $state, video is ${if (isVideoCall) "enabled" else "disabled"}") - if (isVideoCall) { - videoEnabled.value = true - startTimer() - } else { - videoEnabled.value = false - stopTimer() - } - } - } - - override fun onAudioDeviceChanged(core: Core, audioDevice: AudioDevice) { - if (audioDevice.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) { - Log.i("[Controls Fading] Output audio device changed to: ${audioDevice.id}") - nonEarpieceOutputAudioDevice.value = audioDevice.type != AudioDevice.Type.Earpiece - } - } - } - - init { - coreContext.core.addListener(listener) - - areControlsHidden.value = false - isVideoPreviewHidden.value = false - isVideoPreviewResizedForPip.value = false - nonEarpieceOutputAudioDevice.value = coreContext.core.outputAudioDevice?.type != AudioDevice.Type.Earpiece - - val isVideoCall = coreContext.isVideoCallOrConferenceActive() - videoEnabled.value = isVideoCall - if (isVideoCall) { - startTimer() - } - - proximitySensorEnabled.value = shouldEnableProximitySensor() - proximitySensorEnabled.addSource(videoEnabled) { - proximitySensorEnabled.value = shouldEnableProximitySensor() - } - proximitySensorEnabled.addSource(nonEarpieceOutputAudioDevice) { - proximitySensorEnabled.value = shouldEnableProximitySensor() - } - } - - override fun onCleared() { - coreContext.core.removeListener(listener) - stopTimer() - - super.onCleared() - } - - fun showMomentarily() { - stopTimer() - startTimer() - } - - fun disable(disable: Boolean) { - disabled = disable - if (disabled) { - stopTimer() - } else { - startTimer() - } - } - - private fun shouldEnableProximitySensor(): Boolean { - return !(videoEnabled.value ?: false) && !(nonEarpieceOutputAudioDevice.value ?: false) - } - - private fun stopTimer() { - timer?.cancel() - - areControlsHidden.value = false - } - - private fun startTimer() { - timer?.cancel() - if (disabled) return - - timer = Timer("Hide UI controls scheduler") - timer?.schedule( - object : TimerTask() { - override fun run() { - viewModelScope.launch { - withContext(Dispatchers.Main) { - val videoEnabled = coreContext.isVideoCallOrConferenceActive() - areControlsHidden.postValue(videoEnabled) - } - } - } - }, - 3000 - ) - } -} diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsViewModel.kt deleted file mode 100644 index 094cd64c2..000000000 --- a/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsViewModel.kt +++ /dev/null @@ -1,484 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.call.viewmodels - -import android.Manifest -import android.animation.ValueAnimator -import android.content.Context -import android.os.Vibrator -import android.view.animation.LinearInterpolator -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlin.math.max -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.main.dialer.NumpadDigitListener -import org.linphone.compatibility.Compatibility -import org.linphone.core.* -import org.linphone.core.tools.Log -import org.linphone.utils.* -import org.linphone.utils.Event - -class ControlsViewModel : ViewModel() { - val isMicrophoneMuted = MutableLiveData() - - val isMuteMicrophoneEnabled = MutableLiveData() - - val isSpeakerSelected = MutableLiveData() - - val isBluetoothHeadsetSelected = MutableLiveData() - - val isVideoAvailable = MutableLiveData() - - val isVideoEnabled = MutableLiveData() - - val isVideoUpdateInProgress = MutableLiveData() - - val showSwitchCamera = MutableLiveData() - - val isPauseEnabled = MutableLiveData() - - val isRecording = MutableLiveData() - - val isConferencingAvailable = MutableLiveData() - - val unreadMessagesCount = MutableLiveData() - - val numpadVisibility = MutableLiveData() - - val optionsVisibility = MutableLiveData() - - val audioRoutesSelected = MutableLiveData() - - val audioRoutesEnabled = MutableLiveData() - - val takeScreenshotEnabled = MutableLiveData() - - val chatClickedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val addCallClickedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val transferCallClickedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val askAudioRecordPermissionEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val askCameraPermissionEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val somethingClickedEvent = MutableLiveData>() - - val chatAllowed = !corePreferences.disableChat - - private val vibrator = coreContext.context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator - - val chatUnreadCountTranslateY = MutableLiveData() - - val optionsMenuTranslateY = MutableLiveData() - - val audioRoutesMenuTranslateY = MutableLiveData() - - val toggleNumpadEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - private val bounceAnimator: ValueAnimator by lazy { - ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.tabs_fragment_unread_count_bounce_offset), 0f).apply { - addUpdateListener { - val value = it.animatedValue as Float - chatUnreadCountTranslateY.value = -value - } - interpolator = LinearInterpolator() - duration = 250 - repeatMode = ValueAnimator.REVERSE - repeatCount = ValueAnimator.INFINITE - } - } - - private val optionsMenuAnimator: ValueAnimator by lazy { - ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.call_options_menu_translate_y), 0f).apply { - addUpdateListener { - val value = it.animatedValue as Float - optionsMenuTranslateY.value = value - } - duration = if (corePreferences.enableAnimations) 500 else 0 - } - } - - private val audioRoutesMenuAnimator: ValueAnimator by lazy { - ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.call_audio_routes_menu_translate_y), 0f).apply { - addUpdateListener { - val value = it.animatedValue as Float - audioRoutesMenuTranslateY.value = value - } - duration = if (corePreferences.enableAnimations) 500 else 0 - } - } - - val onKeyClick: NumpadDigitListener = object : NumpadDigitListener { - override fun handleClick(key: Char) { - coreContext.core.playDtmf(key, 1) - somethingClickedEvent.value = Event(true) - coreContext.core.currentCall?.sendDtmf(key) - - if (vibrator.hasVibrator() && corePreferences.dtmfKeypadVibration) { - Compatibility.eventVibration(vibrator) - } - } - - override fun handleLongClick(key: Char): Boolean { - return true - } - } - - private val listener: CoreListenerStub = object : CoreListenerStub() { - override fun onMessageReceived(core: Core, chatRoom: ChatRoom, message: ChatMessage) { - updateUnreadChatCount() - } - - override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) { - updateUnreadChatCount() - } - - override fun onCallStateChanged( - core: Core, - call: Call, - state: Call.State, - message: String - ) { - if (state == Call.State.StreamsRunning) { - isVideoUpdateInProgress.value = false - } - - if (coreContext.isVideoCallOrConferenceActive() && !PermissionHelper.get().hasCameraPermission()) { - askCameraPermissionEvent.value = Event(Manifest.permission.CAMERA) - } - - updateUI() - } - - override fun onAudioDeviceChanged(core: Core, audioDevice: AudioDevice) { - Log.i("[Call] Audio device changed: ${audioDevice.deviceName}") - updateSpeakerState() - updateBluetoothHeadsetState() - } - - override fun onAudioDevicesListUpdated(core: Core) { - Log.i("[Call] Audio devices list updated") - val wasBluetoothPreviouslyAvailable = audioRoutesEnabled.value == true - updateAudioRoutesState() - - if (AudioRouteUtils.isHeadsetAudioRouteAvailable()) { - AudioRouteUtils.routeAudioToHeadset() - } else if (!wasBluetoothPreviouslyAvailable && corePreferences.routeAudioToBluetoothIfAvailable) { - // Only attempt to route audio to bluetooth automatically when bluetooth device is connected - if (AudioRouteUtils.isBluetoothAudioRouteAvailable()) { - AudioRouteUtils.routeAudioToBluetooth() - } - } - } - } - - init { - coreContext.core.addListener(listener) - val currentCall = coreContext.core.currentCall - - updateMuteMicState() - updateAudioRelated() - updateUnreadChatCount() - - numpadVisibility.value = false - optionsVisibility.value = false - audioRoutesSelected.value = false - - isRecording.value = currentCall?.isRecording - isVideoUpdateInProgress.value = false - showSwitchCamera.value = coreContext.showSwitchCameraButton() - - chatUnreadCountTranslateY.value = 0f - optionsMenuTranslateY.value = AppUtils.getDimension(R.dimen.call_options_menu_translate_y) - audioRoutesMenuTranslateY.value = AppUtils.getDimension(R.dimen.call_audio_routes_menu_translate_y) - - takeScreenshotEnabled.value = corePreferences.showScreenshotButton - - updateUI() - if (corePreferences.enableAnimations) bounceAnimator.start() - } - - override fun onCleared() { - if (corePreferences.enableAnimations) bounceAnimator.end() - optionsMenuAnimator.end() - audioRoutesMenuAnimator.end() - coreContext.core.removeListener(listener) - super.onCleared() - } - - fun updateUnreadChatCount() { - unreadMessagesCount.value = coreContext.core.unreadChatMessageCountFromActiveLocals - } - - fun toggleMuteMicrophone() { - if (!PermissionHelper.get().hasRecordAudioPermission()) { - askAudioRecordPermissionEvent.value = Event(Manifest.permission.RECORD_AUDIO) - return - } - - somethingClickedEvent.value = Event(true) - val micEnabled = coreContext.core.isMicEnabled - coreContext.core.isMicEnabled = !micEnabled - updateMuteMicState() - } - - fun toggleSpeaker() { - somethingClickedEvent.value = Event(true) - if (AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed()) { - forceEarpieceAudioRoute() - } else { - forceSpeakerAudioRoute() - } - } - - fun switchCamera() { - somethingClickedEvent.value = Event(true) - coreContext.switchCamera() - } - - fun terminateCall() { - val core = coreContext.core - when { - core.currentCall != null -> core.currentCall?.terminate() - core.conference?.isIn == true -> core.terminateConference() - else -> core.terminateAllCalls() - } - } - - fun toggleVideo() { - if (!PermissionHelper.get().hasCameraPermission()) { - askCameraPermissionEvent.value = Event(Manifest.permission.CAMERA) - return - } - - val core = coreContext.core - val currentCall = core.currentCall - val conference = core.conference - - if (conference != null && conference.isIn) { - val params = core.createConferenceParams() - val videoEnabled = conference.currentParams.isVideoEnabled - params.isVideoEnabled = !videoEnabled - Log.i("[Controls VM] Conference current param for video is $videoEnabled") - conference.updateParams(params) - } else if (currentCall != null) { - val state = currentCall.state - if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) - return - - isVideoUpdateInProgress.value = true - val params = core.createCallParams(currentCall) - params?.isVideoEnabled = !currentCall.currentParams.isVideoEnabled - currentCall.update(params) - } - } - - fun toggleOptionsMenu() { - somethingClickedEvent.value = Event(true) - optionsVisibility.value = optionsVisibility.value != true - if (optionsVisibility.value == true) { - optionsMenuAnimator.start() - } else { - optionsMenuAnimator.reverse() - } - } - - fun toggleNumpadVisibility() { - somethingClickedEvent.value = Event(true) - numpadVisibility.value = numpadVisibility.value != true - toggleNumpadEvent.value = Event(numpadVisibility.value ?: true) - } - - fun toggleRoutesMenu() { - somethingClickedEvent.value = Event(true) - audioRoutesSelected.value = audioRoutesSelected.value != true - if (audioRoutesSelected.value == true) { - audioRoutesMenuAnimator.start() - } else { - audioRoutesMenuAnimator.reverse() - } - } - - fun toggleRecording(closeMenu: Boolean) { - somethingClickedEvent.value = Event(true) - - val core = coreContext.core - val currentCall = core.currentCall - val conference = core.conference - - when { - currentCall != null -> { - if (currentCall.isRecording) { - currentCall.stopRecording() - } else { - currentCall.startRecording() - } - isRecording.value = currentCall.isRecording - } - conference != null -> { - val path = LinphoneUtils.getRecordingFilePathForConference() - if (conference.isRecording) { - conference.stopRecording() - } else { - conference.startRecording(path) - } - isRecording.value = conference.isRecording - } - else -> { - isRecording.value = false - } - } - - if (closeMenu) toggleOptionsMenu() - } - - fun onChatClicked() { - chatClickedEvent.value = Event(true) - } - - fun onAddCallClicked() { - addCallClickedEvent.value = Event(true) - toggleOptionsMenu() - } - - fun onTransferCallClicked() { - transferCallClickedEvent.value = Event(true) - toggleOptionsMenu() - } - - fun startConference() { - somethingClickedEvent.value = Event(true) - - val core = coreContext.core - val currentCallVideoEnabled = core.currentCall?.currentParams?.isVideoEnabled ?: false - - val params = core.createConferenceParams() - params.isVideoEnabled = currentCallVideoEnabled - Log.i("[Call] Setting videoEnabled to [$currentCallVideoEnabled] in conference params") - - val conference = core.conference ?: core.createConferenceWithParams(params) - conference?.addParticipants(core.calls) - - toggleOptionsMenu() - } - - fun forceEarpieceAudioRoute() { - somethingClickedEvent.value = Event(true) - if (AudioRouteUtils.isHeadsetAudioRouteAvailable()) { - Log.i("[Call] Headset found, route audio to it instead of earpiece") - AudioRouteUtils.routeAudioToHeadset() - } else { - AudioRouteUtils.routeAudioToEarpiece() - } - } - - fun forceSpeakerAudioRoute() { - somethingClickedEvent.value = Event(true) - AudioRouteUtils.routeAudioToSpeaker() - } - - fun forceBluetoothAudioRoute() { - somethingClickedEvent.value = Event(true) - AudioRouteUtils.routeAudioToBluetooth() - } - - fun updateMuteMicState() { - isMicrophoneMuted.value = !PermissionHelper.get().hasRecordAudioPermission() || !coreContext.core.isMicEnabled - isMuteMicrophoneEnabled.value = coreContext.core.currentCall != null || coreContext.core.conference?.isIn == true - } - - private fun updateAudioRelated() { - updateSpeakerState() - updateBluetoothHeadsetState() - updateAudioRoutesState() - } - - private fun updateUI() { - val currentCall = coreContext.core.currentCall - updateVideoAvailable() - updateVideoEnabled() - isPauseEnabled.value = currentCall != null && !currentCall.mediaInProgress() - isMuteMicrophoneEnabled.value = currentCall != null || coreContext.core.conference?.isIn == true - updateConferenceState() - - // Check periodically until mediaInProgress is false - if (currentCall != null && currentCall.mediaInProgress()) { - viewModelScope.launch { - delay(1000) - updateUI() - } - } - } - - private fun updateSpeakerState() { - isSpeakerSelected.value = AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed() - } - - private fun updateAudioRoutesState() { - val bluetoothDeviceAvailable = AudioRouteUtils.isBluetoothAudioRouteAvailable() - audioRoutesEnabled.value = bluetoothDeviceAvailable - if (!bluetoothDeviceAvailable) { - audioRoutesSelected.value = false - } - } - - private fun updateBluetoothHeadsetState() { - isBluetoothHeadsetSelected.value = AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed() - } - - private fun updateVideoAvailable() { - val core = coreContext.core - val currentCall = core.currentCall - isVideoAvailable.value = (core.isVideoCaptureEnabled || core.isVideoPreviewEnabled) && - ( - (currentCall != null && !currentCall.mediaInProgress()) || - core.conference?.isIn == true - ) - } - - private fun updateVideoEnabled() { - val enabled = coreContext.isVideoCallOrConferenceActive() - isVideoEnabled.value = enabled - } - - private fun updateConferenceState() { - val core = coreContext.core - isConferencingAvailable.value = core.callsNb > max(1, core.conference?.participantCount ?: 0) && !core.soundResourcesLocked() - } -} diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/IncomingCallViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/IncomingCallViewModel.kt deleted file mode 100644 index f1dc0bfb6..000000000 --- a/app/src/main/java/org/linphone/activities/call/viewmodels/IncomingCallViewModel.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.call.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.core.* -import org.linphone.utils.Event - -class IncomingCallViewModelFactory(private val call: Call) : - ViewModelProvider.NewInstanceFactory() { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return IncomingCallViewModel(call) as T - } -} - -class IncomingCallViewModel(call: Call) : CallViewModel(call) { - val screenLocked = MutableLiveData() - - val earlyMediaVideoEnabled = MutableLiveData() - - val inviteWithVideo = MutableLiveData() - - private val listener = object : CoreListenerStub() { - override fun onCallStateChanged( - core: Core, - call: Call, - state: Call.State, - message: String - ) { - if (core.callsNb == 0) { - callEndedEvent.value = Event(true) - } - } - } - - init { - coreContext.core.addListener(listener) - - screenLocked.value = false - inviteWithVideo.value = call.remoteParams?.isVideoEnabled == true && coreContext.core.videoActivationPolicy.automaticallyAccept - earlyMediaVideoEnabled.value = corePreferences.acceptEarlyMedia && - call.state == Call.State.IncomingEarlyMedia && - call.currentParams.isVideoEnabled - } - - override fun onCleared() { - coreContext.core.removeListener(listener) - super.onCleared() - } - - fun answer(doAction: Boolean) { - if (doAction) coreContext.answerCall(call) - } - - fun decline(doAction: Boolean) { - if (doAction) coreContext.declineCall(call) - } -} diff --git a/app/src/main/java/org/linphone/activities/call/views/AnswerDeclineIncomingCallButtons.kt b/app/src/main/java/org/linphone/activities/call/views/AnswerDeclineIncomingCallButtons.kt deleted file mode 100644 index b030ea7a8..000000000 --- a/app/src/main/java/org/linphone/activities/call/views/AnswerDeclineIncomingCallButtons.kt +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.call.views - -import android.animation.AnimatorSet -import android.animation.ObjectAnimator -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.View -import android.view.View.OnTouchListener -import android.view.animation.LinearInterpolator -import android.widget.LinearLayout -import androidx.databinding.DataBindingUtil -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.call.viewmodels.IncomingCallViewModel -import org.linphone.core.tools.Log -import org.linphone.databinding.CallIncomingAnswerDeclineButtonsBinding - -class AnswerDeclineIncomingCallButtons : LinearLayout { - private lateinit var binding: CallIncomingAnswerDeclineButtonsBinding - private var mBegin = false - private var mDeclineX = 0f - private var mAnswerX = 0f - private var mOldSize = 0f - - private val mAnswerTouchListener = OnTouchListener { view, motionEvent -> - val curX: Float - - when (motionEvent.action) { - MotionEvent.ACTION_DOWN -> { - binding.declineButton.visibility = View.GONE - mAnswerX = motionEvent.x - view.width - mBegin = true - mOldSize = 0f - } - MotionEvent.ACTION_MOVE -> { - curX = motionEvent.x - view.width - view.scrollBy((mAnswerX - curX).toInt(), view.scrollY) - mOldSize -= mAnswerX - curX - mAnswerX = curX - if (mOldSize < -25) mBegin = false - if (curX < (width / 4) - view.width && !mBegin) { - binding.viewModel?.answer(true) - } - } - MotionEvent.ACTION_UP -> { - binding.declineButton.visibility = View.VISIBLE - view.scrollTo(0, view.scrollY) - } - } - true - } - private val mDeclineTouchListener = OnTouchListener { view, motionEvent -> - val curX: Float - - when (motionEvent.action) { - MotionEvent.ACTION_DOWN -> { - binding.answerButton.visibility = View.GONE - mDeclineX = motionEvent.x - } - MotionEvent.ACTION_MOVE -> { - curX = motionEvent.x - view.scrollBy((mDeclineX - curX).toInt(), view.scrollY) - mDeclineX = curX - if (curX > 3 * width / 4) { - binding.viewModel?.decline(true) - } - } - MotionEvent.ACTION_UP -> { - binding.answerButton.visibility = View.VISIBLE - view.scrollTo(0, view.scrollY) - } - } - true - } - - constructor(context: Context) : super(context) { - init(context) - } - - constructor(context: Context, attrs: AttributeSet) : super( - context, - attrs - ) { - init(context) - } - - constructor( - context: Context, - attrs: AttributeSet, - defStyleAttr: Int - ) : super(context, attrs, defStyleAttr) { - init(context) - } - - fun setViewModel(viewModel: IncomingCallViewModel) { - binding.viewModel = viewModel - - updateSlideMode() - } - - private fun init(context: Context) { - binding = DataBindingUtil.inflate( - LayoutInflater.from(context), R.layout.call_incoming_answer_decline_buttons, this, true - ) - - updateSlideMode() - configureAnimation() - } - - private fun updateSlideMode() { - val slideMode = binding.viewModel?.screenLocked?.value == true - Log.i("[Call Incoming Decline Button] Slide mode is $slideMode") - if (slideMode) { - binding.answerButton.setOnTouchListener(mAnswerTouchListener) - binding.declineButton.setOnTouchListener(mDeclineTouchListener) - } - } - - private fun configureAnimation() { - if (!corePreferences.enableAnimations) return - - val accept1 = ObjectAnimator.ofFloat(binding.arrowAccept1, "alpha", 1f, 0.6f, 0.4f, 1f).apply { - repeatCount = ObjectAnimator.INFINITE - repeatMode = ObjectAnimator.RESTART - } - - val accept2 = ObjectAnimator.ofFloat(binding.arrowAccept2, "alpha", 0.6f, 1f, 0.4f, 0.6f).apply { - repeatCount = ObjectAnimator.INFINITE - repeatMode = ObjectAnimator.RESTART - } - - val accept3 = ObjectAnimator.ofFloat(binding.arrowAccept3, "alpha", 0.4f, 0.6f, 1f, 0.4f).apply { - repeatCount = ObjectAnimator.INFINITE - repeatMode = ObjectAnimator.RESTART - } - - val hangup1 = ObjectAnimator.ofFloat(binding.arrowHangup1, "alpha", 1f, 0.6f, 0.4f, 1f).apply { - repeatCount = ObjectAnimator.INFINITE - repeatMode = ObjectAnimator.RESTART - } - - val hangup2 = ObjectAnimator.ofFloat(binding.arrowHangup2, "alpha", 0.6f, 1f, 0.4f, 0.6f).apply { - repeatCount = ObjectAnimator.INFINITE - repeatMode = ObjectAnimator.RESTART - } - - val hangup3 = ObjectAnimator.ofFloat(binding.arrowHangup3, "alpha", 0.4f, 0.6f, 1f, 0.4f).apply { - repeatCount = ObjectAnimator.INFINITE - repeatMode = ObjectAnimator.RESTART - } - - AnimatorSet().apply { - duration = 2000 - interpolator = LinearInterpolator() - playTogether(accept1, accept2, accept3, hangup1, hangup2, hangup3) - start() - } - } -} diff --git a/app/src/main/java/org/linphone/activities/call/views/ConferenceParticipantView.kt b/app/src/main/java/org/linphone/activities/call/views/ConferenceParticipantView.kt deleted file mode 100644 index 16499951f..000000000 --- a/app/src/main/java/org/linphone/activities/call/views/ConferenceParticipantView.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.call.views - -import android.content.Context -import android.os.SystemClock -import android.util.AttributeSet -import android.view.LayoutInflater -import android.widget.LinearLayout -import androidx.databinding.DataBindingUtil -import org.linphone.R -import org.linphone.activities.call.data.ConferenceParticipantData -import org.linphone.core.tools.Log -import org.linphone.databinding.CallConferenceParticipantBinding - -class ConferenceParticipantView : LinearLayout { - private lateinit var binding: CallConferenceParticipantBinding - - constructor(context: Context) : super(context) { - init(context) - } - - constructor(context: Context, attrs: AttributeSet) : super( - context, - attrs - ) { - init(context) - } - - constructor( - context: Context, - attrs: AttributeSet, - defStyleAttr: Int - ) : super(context, attrs, defStyleAttr) { - init(context) - } - - fun init(context: Context) { - binding = DataBindingUtil.inflate( - LayoutInflater.from(context), R.layout.call_conference_participant, this, true - ) - } - - fun setData(data: ConferenceParticipantData) { - binding.data = data - - val currentTimeSecs = System.currentTimeMillis() - val participantTime = data.participant.creationTime * 1000 // Linphone timestamps are in seconds - val diff = currentTimeSecs - participantTime - Log.i("[Conference Participant] Participant joined conference at $participantTime == ${diff / 1000} seconds ago.") - binding.callTimer.base = SystemClock.elapsedRealtime() - diff - binding.callTimer.start() - } -} diff --git a/app/src/main/java/org/linphone/activities/call/views/PausedCallView.kt b/app/src/main/java/org/linphone/activities/call/views/PausedCallView.kt deleted file mode 100644 index a5c9ffce4..000000000 --- a/app/src/main/java/org/linphone/activities/call/views/PausedCallView.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.call.views - -import android.content.Context -import android.os.SystemClock -import android.util.AttributeSet -import android.view.LayoutInflater -import android.widget.LinearLayout -import androidx.databinding.DataBindingUtil -import org.linphone.R -import org.linphone.activities.call.viewmodels.CallViewModel -import org.linphone.databinding.CallPausedBinding - -class PausedCallView : LinearLayout { - private lateinit var binding: CallPausedBinding - - constructor(context: Context) : super(context) { - init(context) - } - - constructor(context: Context, attrs: AttributeSet) : super( - context, - attrs - ) { - init(context) - } - - constructor( - context: Context, - attrs: AttributeSet, - defStyleAttr: Int - ) : super(context, attrs, defStyleAttr) { - init(context) - } - - fun init(context: Context) { - binding = DataBindingUtil.inflate( - LayoutInflater.from(context), R.layout.call_paused, this, true - ) - } - - fun setViewModel(viewModel: CallViewModel) { - binding.viewModel = viewModel - - binding.callTimer.base = - SystemClock.elapsedRealtime() - (1000 * viewModel.call.duration) // Linphone timestamps are in seconds - binding.callTimer.start() - } -} diff --git a/app/src/main/java/org/linphone/activities/chat_bubble/ChatBubbleActivity.kt b/app/src/main/java/org/linphone/activities/chat_bubble/ChatBubbleActivity.kt index 08d3d8d1e..9dfa3387c 100644 --- a/app/src/main/java/org/linphone/activities/chat_bubble/ChatBubbleActivity.kt +++ b/app/src/main/java/org/linphone/activities/chat_bubble/ChatBubbleActivity.kt @@ -119,7 +119,7 @@ class ChatBubbleActivity : GenericActivity() { adapter.registerAdapterDataObserver(observer) // Disable context menu on each message - adapter.disableContextMenu() + adapter.disableAdvancedContextMenuOptions() adapter.openContentEvent.observe( this diff --git a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt index 12a69892f..83ebbe442 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt @@ -40,11 +40,11 @@ import org.linphone.activities.main.chat.data.EventData import org.linphone.activities.main.chat.data.EventLogData import org.linphone.activities.main.chat.data.OnContentClickedListener import org.linphone.activities.main.viewmodels.ListTopBarViewModel -import org.linphone.core.ChatMessage -import org.linphone.core.ChatRoomCapabilities -import org.linphone.core.Content -import org.linphone.core.EventLog -import org.linphone.databinding.* +import org.linphone.core.* +import org.linphone.databinding.ChatEventListCellBinding +import org.linphone.databinding.ChatMessageListCellBinding +import org.linphone.databinding.ChatMessageLongPressMenuBindingImpl +import org.linphone.databinding.ChatUnreadMessagesListHeaderBinding import org.linphone.utils.AppUtils import org.linphone.utils.Event import org.linphone.utils.HeaderAdapter @@ -90,6 +90,10 @@ class ChatMessagesListAdapter( MutableLiveData>() } + val callConferenceEvent: MutableLiveData>> by lazy { + MutableLiveData>>() + } + val scrollToChatMessageEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -102,9 +106,13 @@ class ChatMessagesListAdapter( override fun onSipAddressClicked(sipUri: String) { sipUriClickedEvent.value = Event(sipUri) } + + override fun onCallConference(address: String, subject: String?) { + callConferenceEvent.value = Event(Pair(address, subject)) + } } - private var contextMenuDisabled: Boolean = false + private var advancedContextMenuOptionsDisabled: Boolean = false private var unreadMessagesCount: Int = 0 private var firstUnreadMessagePosition: Int = -1 @@ -170,8 +178,8 @@ class ChatMessagesListAdapter( return binding.root } - fun disableContextMenu() { - contextMenuDisabled = true + fun disableAdvancedContextMenuOptions() { + advancedContextMenuOptionsDisabled = true } fun setUnreadMessageCount(count: Int, forceUpdate: Boolean) { @@ -269,8 +277,6 @@ class ChatMessagesListAdapter( executePendingBindings() - if (contextMenuDisabled) return - setContextMenuClickListener { val popupView: ChatMessageLongPressMenuBindingImpl = DataBindingUtil.inflate( LayoutInflater.from(root.context), @@ -292,7 +298,10 @@ class ChatMessagesListAdapter( popupView.copyTextHidden = true totalSize -= itemSize } - if (chatMessage.isOutgoing || chatMessageViewModel.contact.value != null) { + if (chatMessage.isOutgoing || + chatMessageViewModel.contact.value != null || + advancedContextMenuOptionsDisabled + ) { popupView.addToContactsHidden = true totalSize -= itemSize } @@ -300,6 +309,10 @@ class ChatMessagesListAdapter( popupView.replyHidden = true totalSize -= itemSize } + if (advancedContextMenuOptionsDisabled) { + popupView.forwardHidden = true + totalSize -= itemSize + } // When using WRAP_CONTENT instead of real size, fails to place the // popup window above if not enough space is available below diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt index 8c81a62cf..df0e86c83 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt @@ -27,8 +27,12 @@ import android.text.style.UnderlineSpan import android.widget.Toast import androidx.lifecycle.MutableLiveData import androidx.media.AudioFocusRequestCompat +import java.io.BufferedReader +import java.io.FileReader +import java.lang.StringBuilder import java.text.SimpleDateFormat import java.util.* +import java.util.concurrent.TimeUnit import kotlinx.coroutines.* import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.launchIn @@ -40,6 +44,7 @@ import org.linphone.core.tools.Log import org.linphone.utils.AppUtils import org.linphone.utils.FileUtils import org.linphone.utils.ImageUtils +import org.linphone.utils.TimestampUtils class ChatMessageContentData( private val chatMessage: ChatMessage, @@ -56,6 +61,7 @@ class ChatMessageContentData( val isPdf = MutableLiveData() val isGenericFile = MutableLiveData() val isVoiceRecording = MutableLiveData() + val isConferenceSchedule = MutableLiveData() val fileName = MutableLiveData() val filePath = MutableLiveData() @@ -71,6 +77,15 @@ class ChatMessageContentData( val voiceRecordPlayingPosition = MutableLiveData() val isVoiceRecordPlaying = MutableLiveData() + val conferenceSubject = MutableLiveData() + val conferenceDescription = MutableLiveData() + val conferenceParticipantCount = MutableLiveData() + val conferenceDate = MutableLiveData() + val conferenceTime = MutableLiveData() + val conferenceDuration = MutableLiveData() + var conferenceAddress = MutableLiveData() + val showDuration = MutableLiveData() + val isAlone: Boolean get() { var count = 0 @@ -203,6 +218,13 @@ class ChatMessageContentData( spannable.setSpan(UnderlineSpan(), 0, spannable.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) downloadLabel.value = spannable + isImage.value = false + isVideo.value = false + isAudio.value = false + isPdf.value = false + isVoiceRecording.value = false + isConferenceSchedule.value = false + if (content.isFile || (content.isFileTransfer && chatMessage.isOutgoing)) { val path = if (isFileEncrypted) { Log.i("[Content] Content is encrypted, requesting plain file path") @@ -212,21 +234,27 @@ class ChatMessageContentData( } downloadable.value = content.filePath.orEmpty().isEmpty() + val isVoiceRecord = content.isVoiceRecording + isVoiceRecording.value = isVoiceRecord + + val isConferenceIcs = content.isIcalendar + isConferenceSchedule.value = isConferenceIcs + if (path.isNotEmpty()) { Log.i("[Content] Found displayable content: $path") - val isVoiceRecord = content.isVoiceRecording filePath.value = path isImage.value = FileUtils.isExtensionImage(path) isVideo.value = FileUtils.isExtensionVideo(path) && !isVoiceRecord isAudio.value = FileUtils.isExtensionAudio(path) && !isVoiceRecord isPdf.value = FileUtils.isExtensionPdf(path) - isVoiceRecording.value = isVoiceRecord if (isVoiceRecord) { val duration = content.fileDuration // duration is in ms voiceRecordDuration.value = duration formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration) - Log.i("[Voice Recording] Duration is ${voiceRecordDuration.value} ($duration)") + Log.i("[Content] Voice recording duration is ${voiceRecordDuration.value} ($duration)") + } else if (isConferenceIcs) { + parseConferenceInvite(content) } if (isVideo.value == true) { @@ -234,6 +262,9 @@ class ChatMessageContentData( videoPreview.postValue(ImageUtils.getVideoPreview(path)) } } + } else if (isConferenceIcs) { + Log.i("[Content] Found content with icalendar file") + parseConferenceInvite(content) } else { Log.w("[Content] Found ${if (content.isFile) "file" else "file transfer"} content with empty path...") isImage.value = false @@ -241,22 +272,81 @@ class ChatMessageContentData( isAudio.value = false isPdf.value = false isVoiceRecording.value = false + isConferenceSchedule.value = false } - } else { + } else if (content.isFileTransfer) { downloadable.value = true isImage.value = FileUtils.isExtensionImage(fileName.value!!) isVideo.value = FileUtils.isExtensionVideo(fileName.value!!) isAudio.value = FileUtils.isExtensionAudio(fileName.value!!) isPdf.value = FileUtils.isExtensionPdf(fileName.value!!) isVoiceRecording.value = false + isConferenceSchedule.value = false + } else if (content.isIcalendar) { + Log.i("[Content] Found content with icalendar body") + isConferenceSchedule.value = true + parseConferenceInvite(content) + } else { + Log.w("[Content] Found content that's neither a file or a file transfer") } - isGenericFile.value = !isPdf.value!! && !isAudio.value!! && !isVideo.value!! && !isImage.value!! && !isVoiceRecording.value!! + isGenericFile.value = !isPdf.value!! && !isAudio.value!! && !isVideo.value!! && !isImage.value!! && !isVoiceRecording.value!! && !isConferenceSchedule.value!! downloadEnabled.value = !chatMessage.isFileTransferInProgress downloadProgressInt.value = 0 downloadProgressString.value = "0%" } + private fun parseConferenceInvite(content: Content) { + val conferenceInfo = Factory.instance().createConferenceInfoFromIcalendarContent(content) + val conferenceUri = conferenceInfo?.uri?.asStringUriOnly() + if (conferenceInfo != null && conferenceUri != null) { + conferenceAddress.value = conferenceUri!! + Log.i("[Content] Created conference info from ICS with address ${conferenceAddress.value}") + conferenceSubject.value = conferenceInfo.subject + conferenceDescription.value = conferenceInfo.description + + conferenceDate.value = TimestampUtils.dateToString(conferenceInfo.dateTime) + conferenceTime.value = TimestampUtils.timeToString(conferenceInfo.dateTime) + + val minutes = conferenceInfo.duration + val hours = TimeUnit.MINUTES.toHours(minutes.toLong()) + val remainMinutes = minutes - TimeUnit.HOURS.toMinutes(hours).toInt() + conferenceDuration.value = TimestampUtils.durationToString(hours.toInt(), remainMinutes) + showDuration.value = minutes > 0 + + conferenceParticipantCount.value = String.format(AppUtils.getString(R.string.conference_invite_participants_count), conferenceInfo.participants.size + 1) // +1 for organizer + } else if (conferenceInfo == null) { + if (content.filePath != null) { + try { + val br = BufferedReader(FileReader(content.filePath)) + var line: String? + val textBuilder = StringBuilder() + while (br.readLine().also { line = it } != null) { + textBuilder.append(line) + textBuilder.append('\n') + } + br.close() + Log.e("[Content] Failed to create conference info from ICS file [${content.filePath}]: $textBuilder") + } catch (e: Exception) { + Log.e("[Content] Failed to read content of ICS file [${content.filePath}]: $e") + } + } else { + Log.e("[Content] Failed to create conference info from ICS: ${content.utf8Text}") + } + } else if (conferenceInfo.uri == null) { + Log.e("[Content] Failed to find the conference URI in conference info [$conferenceInfo]") + } + } + + fun callConferenceAddress() { + val address = conferenceAddress.value + if (address == null) { + Log.e("[Content] Can't call null conference address!") + return + } + listener?.onCallConference(address, conferenceSubject.value) + } + /** Voice recording specifics */ fun playVoiceRecording() { @@ -359,4 +449,6 @@ interface OnContentClickedListener { fun onContentClicked(content: Content) fun onSipAddressClicked(sipUri: String) + + fun onCallConference(address: String, subject: String?) } diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt index 3f588d008..993a74c99 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt @@ -177,7 +177,7 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes val contentsList = chatMessage.contents for (index in contentsList.indices) { val content = contentsList[index] - if (content.isFileTransfer || content.isFile) { + if (content.isFileTransfer || content.isFile || content.isIcalendar) { val data = ChatMessageContentData(chatMessage, index) data.listener = contentListener list.add(data) @@ -194,6 +194,8 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes } } ).build(spannable) + } else { + Log.e("[Chat Message Data] Unexpected content with type: ${content.type}/${content.subtype}") } } diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListChildData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListChildData.kt index d4a51b937..3052871d9 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListChildData.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListChildData.kt @@ -44,6 +44,6 @@ class DevicesListChildData(private val device: ParticipantDevice) { } fun onClick() { - coreContext.startCall(device.address, true) + coreContext.startCall(device.address, forceZRTP = true) } } diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListGroupData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListGroupData.kt index 3d1e624be..bc3141943 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListGroupData.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListGroupData.kt @@ -68,6 +68,6 @@ class DevicesListGroupData(private val participant: Participant) : GenericContac } fun onClick() { - if (device?.address != null) coreContext.startCall(device.address, true) + if (device?.address != null) coreContext.startCall(device.address, forceZRTP = true) } } diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt index 95b08e3bf..069d90a24 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt @@ -25,16 +25,16 @@ import android.view.View import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager -import org.linphone.LinphoneApplication +import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R import org.linphone.activities.main.MainActivity -import org.linphone.activities.main.chat.adapters.ChatRoomCreationContactsAdapter import org.linphone.activities.main.chat.viewmodels.ChatRoomCreationViewModel import org.linphone.activities.main.fragments.SecureFragment import org.linphone.activities.main.viewmodels.SharedMainViewModel import org.linphone.activities.navigateToChatRoom import org.linphone.activities.navigateToEmptyChatRoom import org.linphone.activities.navigateToGroupInfo +import org.linphone.contact.ContactsSelectionAdapter import org.linphone.core.tools.Log import org.linphone.databinding.ChatRoomCreationFragmentBinding import org.linphone.utils.AppUtils @@ -44,7 +44,7 @@ import org.linphone.utils.PermissionHelper class ChatRoomCreationFragment : SecureFragment() { private lateinit var viewModel: ChatRoomCreationViewModel private lateinit var sharedViewModel: SharedMainViewModel - private lateinit var adapter: ChatRoomCreationContactsAdapter + private lateinit var adapter: ContactsSelectionAdapter override fun getLayoutId(): Int = R.layout.chat_room_creation_fragment @@ -68,9 +68,9 @@ class ChatRoomCreationFragment : SecureFragment binding.viewModel = viewModel - adapter = ChatRoomCreationContactsAdapter(viewLifecycleOwner) - adapter.groupChatEnabled = viewModel.createGroupChat.value == true - adapter.updateSecurity(viewModel.isEncrypted.value == true) + adapter = ContactsSelectionAdapter(viewLifecycleOwner) + adapter.setGroupChatCapabilityRequired(viewModel.createGroupChat.value == true) + adapter.setLimeCapabilityRequired(viewModel.isEncrypted.value == true) binding.contactsList.adapter = adapter val layoutManager = LinearLayoutManager(activity) @@ -101,7 +101,7 @@ class ChatRoomCreationFragment : SecureFragment viewModel.isEncrypted.observe( viewLifecycleOwner ) { - adapter.updateSecurity(it) + adapter.setLimeCapabilityRequired(it) } viewModel.sipContactsSelected.observe( @@ -152,7 +152,7 @@ class ChatRoomCreationFragment : SecureFragment navigateToGroupInfo() } - viewModel.onErrorEvent.observe( + viewModel.onMessageToNotifyEvent.observe( viewLifecycleOwner ) { it.consume { messageResourceId -> @@ -185,8 +185,8 @@ class ChatRoomCreationFragment : SecureFragment val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED if (granted) { Log.i("[Chat Room Creation] READ_CONTACTS permission granted") - LinphoneApplication.coreContext.contactsManager.onReadContactsPermissionGranted() - LinphoneApplication.coreContext.contactsManager.fetchContactsAsync() + coreContext.contactsManager.onReadContactsPermissionGranted() + coreContext.contactsManager.fetchContactsAsync() } else { Log.w("[Chat Room Creation] READ_CONTACTS permission denied") } diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt index f8c2e012e..a2041beb4 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt @@ -161,7 +161,6 @@ class DetailChatRoomFragment : MasterFragment - val path = content.filePath.orEmpty() + var path = content.filePath.orEmpty() if (!File(path).exists()) { (requireActivity() as MainActivity).showSnackBar(R.string.chat_room_file_not_found) } else { + if (path.isEmpty()) { + val name = content.name + if (name != null && name.isNotEmpty()) { + val file = FileUtils.getFileStoragePath(name) + FileUtils.writeIntoFile(content.buffer, file) + path = file.absolutePath + content.filePath = path + Log.i("[Chat Message] Content file path was empty, created file from buffer at $path") + } else if (content.isIcalendar) { + val name = "conference.ics" + val file = FileUtils.getFileStoragePath(name) + FileUtils.writeIntoFile(content.buffer, file) + path = file.absolutePath + content.filePath = path + Log.i("[Chat Message] Content file path was empty, created conference.ics from buffer at $path") + } + } + Log.i("[Chat Message] Opening file: $path") sharedViewModel.contentToOpen.value = content @@ -470,6 +487,14 @@ class DetailChatRoomFragment : MasterFragment + navigateToConferenceWaitingRoom(pair.first, pair.second) + } + } + adapter.scrollToChatMessageEvent.observe( viewLifecycleOwner ) { @@ -542,7 +567,7 @@ class DetailChatRoomFragment : MasterFragment() { val chatRoom = sharedViewModel.selectedChatRoom.value if (chatRoom == null) { Log.e("[Devices] Chat room is null, aborting!") - // (activity as MainActivity).showSnackBar(R.string.error) findNavController().navigateUp() return } diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/EphemeralFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/EphemeralFragment.kt index 3eed8654b..310ca4724 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/EphemeralFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/EphemeralFragment.kt @@ -52,7 +52,6 @@ class EphemeralFragment : SecureFragment() { val chatRoom = sharedViewModel.selectedChatRoom.value if (chatRoom == null) { Log.e("[Ephemeral] Chat room is null, aborting!") - // (activity as MainActivity).showSnackBar(R.string.error) findNavController().navigateUp() return } diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt index 4baa685a3..9e5ea8c16 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt @@ -175,7 +175,7 @@ class GroupInfoFragment : SecureFragment() { dialog.show() } - viewModel.onErrorEvent.observe( + viewModel.onMessageToNotifyEvent.observe( viewLifecycleOwner ) { it.consume { messageResourceId -> diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt index e50e5d0a1..7b422a491 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt @@ -56,7 +56,6 @@ class ImdnFragment : SecureFragment() { val chatRoom = sharedViewModel.selectedChatRoom.value if (chatRoom == null) { Log.e("[IMDN] Chat room is null, aborting!") - // (activity as MainActivity).showSnackBar(R.string.error) findNavController().navigateUp() return } diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt index eff7aedc1..d2626aa41 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt @@ -312,8 +312,6 @@ class MasterChatRoomsFragment : MasterFragment diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt index bb1cc356e..24ced613a 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt @@ -23,43 +23,26 @@ import androidx.lifecycle.MutableLiveData import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R -import org.linphone.activities.main.viewmodels.ErrorReportingViewModel -import org.linphone.contact.ContactsUpdatedListenerStub +import org.linphone.contact.ContactsSelectionViewModel import org.linphone.core.* import org.linphone.core.tools.Log import org.linphone.utils.AppUtils import org.linphone.utils.Event import org.linphone.utils.LinphoneUtils -class ChatRoomCreationViewModel : ErrorReportingViewModel() { +class ChatRoomCreationViewModel : ContactsSelectionViewModel() { val chatRoomCreatedEvent: MutableLiveData> by lazy { MutableLiveData>() } val createGroupChat = MutableLiveData() - val sipContactsSelected = MutableLiveData() - val isEncrypted = MutableLiveData() - val contactsList = MutableLiveData>() - val waitForChatRoomCreation = MutableLiveData() - val selectedAddresses = MutableLiveData>() - - val filter = MutableLiveData() - private var previousFilter = "" - val limeAvailable: Boolean = LinphoneUtils.isLimeAvailable() - private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() { - override fun onContactsUpdated() { - Log.i("[Chat Room Creation] Contacts have changed") - updateContactsList() - } - } - private val listener = object : ChatRoomListenerStub() { override fun onStateChanged(room: ChatRoom, state: ChatRoom.State) { if (state == ChatRoom.State.Created) { @@ -69,25 +52,18 @@ class ChatRoomCreationViewModel : ErrorReportingViewModel() { } else if (state == ChatRoom.State.CreationFailed) { Log.e("[Chat Room Creation] Group chat room creation has failed !") waitForChatRoomCreation.value = false - onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack) + onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack) } } } init { createGroupChat.value = false - sipContactsSelected.value = coreContext.contactsManager.shouldDisplaySipContactsList() isEncrypted.value = false - - selectedAddresses.value = arrayListOf() - - coreContext.contactsManager.addListener(contactsUpdatedListener) waitForChatRoomCreation.value = false } override fun onCleared() { - coreContext.contactsManager.removeListener(contactsUpdatedListener) - super.onCleared() } @@ -95,55 +71,6 @@ class ChatRoomCreationViewModel : ErrorReportingViewModel() { isEncrypted.value = encrypted } - fun applyFilter() { - val filterValue = filter.value.orEmpty() - if (previousFilter == filterValue) return - - if (previousFilter.isNotEmpty() && previousFilter.length > filterValue.length) { - coreContext.contactsManager.magicSearch.resetSearchCache() - } - previousFilter = filterValue - - updateContactsList() - } - - fun updateContactsList() { - val domain = if (sipContactsSelected.value == true) coreContext.core.defaultAccount?.params?.domain ?: "" else "" - val results = coreContext.contactsManager.magicSearch.getContactListFromFilter(filter.value.orEmpty(), domain) - - val list = arrayListOf() - for (result in results) { - list.add(result) - } - contactsList.value = list - } - - fun toggleSelectionForSearchResult(searchResult: SearchResult) { - val address = searchResult.address - if (address != null) { - toggleSelectionForAddress(address) - } - } - - fun toggleSelectionForAddress(address: Address) { - val list = arrayListOf
() - list.addAll(selectedAddresses.value.orEmpty()) - - val found = list.find { - it.weakEqual(address) - } - - if (found != null) { - list.remove(found) - } else { - val contact = coreContext.contactsManager.findContactByAddress(address) - if (contact != null) address.displayName = contact.fullName - list.add(address) - } - - selectedAddresses.value = list - } - fun createOneToOneChat(searchResult: SearchResult) { waitForChatRoomCreation.value = true val defaultAccount = coreContext.core.defaultAccount @@ -152,7 +79,7 @@ class ChatRoomCreationViewModel : ErrorReportingViewModel() { val address = searchResult.address ?: coreContext.core.interpretUrl(searchResult.phoneNumber ?: "") if (address == null) { Log.e("[Chat Room Creation] Can't get a valid address from search result $searchResult") - onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack) + onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack) waitForChatRoomCreation.value = false return } diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt index b84b82090..3a06cc41f 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt @@ -20,6 +20,9 @@ package org.linphone.activities.main.chat.viewmodels import android.animation.ValueAnimator +import android.graphics.Typeface +import android.text.SpannableStringBuilder +import android.text.style.StyleSpan import android.view.animation.LinearInterpolator import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -60,7 +63,7 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf val lastUpdate = MutableLiveData() - val lastMessageText = MutableLiveData() + val lastMessageText = MutableLiveData() val callInProgress = MutableLiveData() @@ -113,7 +116,7 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf override fun onContactsUpdated() { Log.i("[Chat Room] Contacts have changed") contactLookup() - updateLastMessageToDisplay() + formatLastMessage(chatRoom.lastMessageInHistory) } } @@ -150,11 +153,11 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf override fun onChatMessageReceived(chatRoom: ChatRoom, eventLog: EventLog) { unreadMessagesCount.value = chatRoom.unreadMessagesCount - lastMessageText.value = formatLastMessage(eventLog.chatMessage) + formatLastMessage(eventLog.chatMessage) } override fun onChatMessageSent(chatRoom: ChatRoom, eventLog: EventLog) { - lastMessageText.value = formatLastMessage(eventLog.chatMessage) + formatLastMessage(eventLog.chatMessage) } override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) { @@ -199,7 +202,7 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) { Log.i("[Chat Room] Ephemeral message deleted, updated last message displayed") - updateLastMessageToDisplay() + formatLastMessage(chatRoom.lastMessageInHistory) } override fun onEphemeralEvent(chatRoom: ChatRoom, eventLog: EventLog) { @@ -216,6 +219,7 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf chatRoom.addListener(chatRoomListener) coreContext.contactsManager.addListener(contactsUpdatedListener) + formatLastMessage(chatRoom.lastMessageInHistory) unreadMessagesCount.value = chatRoom.unreadMessagesCount lastUpdate.value = TimestampUtils.toString(chatRoom.lastUpdateTime, true) @@ -226,7 +230,7 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf contactLookup() updateParticipants() - updateLastMessageToDisplay() + formatLastMessage(chatRoom.lastMessageInHistory) callInProgress.value = chatRoom.core.callsNb > 0 updateRemotesComposing() @@ -277,22 +281,36 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf } fun updateLastMessageToDisplay() { - lastMessageText.value = formatLastMessage(chatRoom.lastMessageInHistory) + formatLastMessage(chatRoom.lastMessageInHistory) } - private fun formatLastMessage(msg: ChatMessage?): String { - if (msg == null) return "" + private fun formatLastMessage(msg: ChatMessage?) { + val builder = SpannableStringBuilder() + if (msg == null) { + lastMessageText.value = builder + return + } val sender: String = coreContext.contactsManager.findContactByAddress(msg.fromAddress)?.fullName ?: LinphoneUtils.getDisplayName(msg.fromAddress) - var body = "" + builder.append(sender) + builder.append(": ") + for (content in msg.contents) { - if (content.isFile || content.isFileTransfer) body += content.name + " " - else if (content.isText) body += content.utf8Text + " " + if (content.isIcalendar) { + val body = AppUtils.getString(R.string.conference_invitation) + builder.append(body) + builder.setSpan(StyleSpan(Typeface.ITALIC), builder.length - body.length, builder.length, 0) + } else if (content.isFile || content.isFileTransfer) { + builder.append(content.name + " ") + } else if (content.isText) { + builder.append(content.utf8Text + " ") + } } - return "$sender: $body" + builder.trim() + lastMessageText.value = builder } private fun searchMatchingContact() { diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt index 1e9982c5f..7982d9fcf 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt @@ -22,7 +22,7 @@ package org.linphone.activities.main.chat.viewmodels import androidx.lifecycle.MutableLiveData import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R -import org.linphone.activities.main.viewmodels.ErrorReportingViewModel +import org.linphone.activities.main.viewmodels.MessageNotifierViewModel import org.linphone.compatibility.Compatibility import org.linphone.contact.ContactsUpdatedListenerStub import org.linphone.core.* @@ -30,7 +30,7 @@ import org.linphone.core.tools.Log import org.linphone.utils.Event import org.linphone.utils.LinphoneUtils -class ChatRoomsListViewModel : ErrorReportingViewModel() { +class ChatRoomsListViewModel : MessageNotifierViewModel() { val chatRooms = MutableLiveData>() val contactsUpdatedEvent: MutableLiveData> by lazy { @@ -60,7 +60,7 @@ class ChatRoomsListViewModel : ErrorReportingViewModel() { } } else if (state == ChatRoom.State.TerminationFailed) { Log.e("[Chat Rooms] Group chat room removal for address ${chatRoom.peerAddress.asStringUriOnly()} has failed !") - onErrorEvent.value = Event(R.string.chat_room_removal_failed_snack) + onMessageToNotifyEvent.value = Event(R.string.chat_room_removal_failed_snack) } } diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt index 7bfd94e48..807698f62 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt @@ -27,7 +27,7 @@ import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.activities.main.chat.GroupChatRoomMember import org.linphone.activities.main.chat.data.GroupInfoParticipantData -import org.linphone.activities.main.viewmodels.ErrorReportingViewModel +import org.linphone.activities.main.viewmodels.MessageNotifierViewModel import org.linphone.core.* import org.linphone.core.tools.Log import org.linphone.utils.Event @@ -41,7 +41,7 @@ class GroupInfoViewModelFactory(private val chatRoom: ChatRoom?) : } } -class GroupInfoViewModel(val chatRoom: ChatRoom?) : ErrorReportingViewModel() { +class GroupInfoViewModel(val chatRoom: ChatRoom?) : MessageNotifierViewModel() { val createdChatRoomEvent = MutableLiveData>() val updatedChatRoomEvent = MutableLiveData>() @@ -69,7 +69,7 @@ class GroupInfoViewModel(val chatRoom: ChatRoom?) : ErrorReportingViewModel() { } else if (state == ChatRoom.State.CreationFailed) { Log.e("[Chat Room Group Info] Group chat room creation has failed !") waitForChatRoomCreation.value = false - onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack) + onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack) } } @@ -142,7 +142,7 @@ class GroupInfoViewModel(val chatRoom: ChatRoom?) : ErrorReportingViewModel() { if (chatRoom == null) { Log.e("[Chat Room Group Info] Couldn't create chat room!") waitForChatRoomCreation.value = false - onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack) + onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack) } } diff --git a/app/src/main/java/org/linphone/activities/main/conference/adapters/ScheduledConferencesAdapter.kt b/app/src/main/java/org/linphone/activities/main/conference/adapters/ScheduledConferencesAdapter.kt new file mode 100644 index 000000000..a3ce4ac64 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/conference/adapters/ScheduledConferencesAdapter.kt @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.main.conference.adapters + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.linphone.R +import org.linphone.activities.main.conference.data.ScheduledConferenceData +import org.linphone.core.Address +import org.linphone.databinding.ConferenceScheduleCellBinding +import org.linphone.databinding.ConferenceScheduleListHeaderBinding +import org.linphone.utils.Event +import org.linphone.utils.HeaderAdapter +import org.linphone.utils.TimestampUtils + +class ScheduledConferencesAdapter( + private val viewLifecycleOwner: LifecycleOwner +) : ListAdapter(ConferenceInfoDiffCallback()), + HeaderAdapter { + val copyAddressToClipboardEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val joinConferenceEvent: MutableLiveData>> by lazy { + MutableLiveData>>() + } + + val deleteConferenceInfoEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScheduledConferencesAdapter.ViewHolder { + val binding: ConferenceScheduleCellBinding = DataBindingUtil.inflate( + LayoutInflater.from(parent.context), + R.layout.conference_schedule_cell, parent, false + ) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + (holder as ScheduledConferencesAdapter.ViewHolder).bind(getItem(position)) + } + + override fun displayHeaderForPosition(position: Int): Boolean { + if (position >= itemCount) return false + val conferenceInfo = getItem(position) + val previousPosition = position - 1 + return if (previousPosition >= 0) { + val previousItem = getItem(previousPosition) + !TimestampUtils.isSameDay(previousItem.conferenceInfo.dateTime, conferenceInfo.conferenceInfo.dateTime) + } else true + } + + override fun getHeaderViewForPosition(context: Context, position: Int): View { + val data = getItem(position) + val binding: ConferenceScheduleListHeaderBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.conference_schedule_list_header, null, false + ) + binding.title = formatDate(context, data.conferenceInfo.dateTime) + binding.executePendingBindings() + return binding.root + } + + private fun formatDate(context: Context, date: Long): String { + if (TimestampUtils.isToday(date)) { + return context.getString(R.string.today) + } + return TimestampUtils.toString(date, onlyDate = true, shortDate = false, hideYear = false) + } + + inner class ViewHolder( + val binding: ConferenceScheduleCellBinding + ) : RecyclerView.ViewHolder(binding.root) { + fun bind(conferenceData: ScheduledConferenceData) { + with(binding) { + data = conferenceData + + lifecycleOwner = viewLifecycleOwner + + setCopyAddressClickListener { + val address = conferenceData.conferenceInfo.uri + if (address != null) { + copyAddressToClipboardEvent.value = Event(address) + } + } + + setJoinConferenceClickListener { + val address = conferenceData.conferenceInfo.uri + if (address != null) { + joinConferenceEvent.value = Event(Pair(address.asStringUriOnly(), conferenceData.conferenceInfo.subject)) + } + } + + setDeleteConferenceClickListener { + deleteConferenceInfoEvent.value = Event(conferenceData) + } + + executePendingBindings() + } + } + } +} + +private class ConferenceInfoDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ScheduledConferenceData, + newItem: ScheduledConferenceData + ): Boolean { + return oldItem.conferenceInfo == newItem.conferenceInfo + } + + override fun areContentsTheSame( + oldItem: ScheduledConferenceData, + newItem: ScheduledConferenceData + ): Boolean { + return false + } +} diff --git a/app/src/main/java/org/linphone/activities/main/conference/data/ConferenceSchedulingParticipantData.kt b/app/src/main/java/org/linphone/activities/main/conference/data/ConferenceSchedulingParticipantData.kt new file mode 100644 index 000000000..71d9f0f87 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/conference/data/ConferenceSchedulingParticipantData.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.main.conference.data + +import org.linphone.contact.GenericContactData +import org.linphone.core.Address +import org.linphone.utils.LinphoneUtils + +class ConferenceSchedulingParticipantData( + private val sipAddress: Address, + val showLimeBadge: Boolean +) : + GenericContactData(sipAddress) { + val sipUri: String get() = LinphoneUtils.getDisplayableAddress(sipAddress) +} diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/SharedCallViewModel.kt b/app/src/main/java/org/linphone/activities/main/conference/data/Duration.kt similarity index 64% rename from app/src/main/java/org/linphone/activities/call/viewmodels/SharedCallViewModel.kt rename to app/src/main/java/org/linphone/activities/main/conference/data/Duration.kt index a10b96fe3..61a6ee696 100644 --- a/app/src/main/java/org/linphone/activities/call/viewmodels/SharedCallViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/conference/data/Duration.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2020 Belledonne Communications SARL. + * Copyright (c) 2010-2021 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,14 +17,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.activities.call.viewmodels +package org.linphone.activities.main.conference.data -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.utils.Event +class Duration(val value: Int, val display: String) : Comparable { + override fun toString(): String { + return display + } -class SharedCallViewModel : ViewModel() { - val toggleDrawerEvent = MutableLiveData>() - - val resetHiddenInterfaceTimerInVideoCallEvent = MutableLiveData>() + override fun compareTo(other: Duration): Int { + return value.compareTo(other.value) + } } diff --git a/app/src/main/java/org/linphone/activities/main/conference/data/ScheduledConferenceData.kt b/app/src/main/java/org/linphone/activities/main/conference/data/ScheduledConferenceData.kt new file mode 100644 index 000000000..feff7598f --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/conference/data/ScheduledConferenceData.kt @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.main.conference.data + +import androidx.lifecycle.MutableLiveData +import java.util.concurrent.TimeUnit +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.ConferenceInfo +import org.linphone.core.tools.Log +import org.linphone.utils.LinphoneUtils +import org.linphone.utils.TimestampUtils + +class ScheduledConferenceData(val conferenceInfo: ConferenceInfo) { + val expanded = MutableLiveData() + + val address = MutableLiveData() + val subject = MutableLiveData() + val description = MutableLiveData() + val time = MutableLiveData() + val date = MutableLiveData() + val duration = MutableLiveData() + val organizer = MutableLiveData() + val participantsShort = MutableLiveData() + val participantsExpanded = MutableLiveData() + val showDuration = MutableLiveData() + + init { + expanded.value = false + + address.value = conferenceInfo.uri?.asStringUriOnly() + subject.value = conferenceInfo.subject + description.value = conferenceInfo.description + + time.value = TimestampUtils.timeToString(conferenceInfo.dateTime) + date.value = TimestampUtils.toString(conferenceInfo.dateTime, onlyDate = true, shortDate = false, hideYear = false) + + val minutes = conferenceInfo.duration + val hours = TimeUnit.MINUTES.toHours(minutes.toLong()) + val remainMinutes = minutes - TimeUnit.HOURS.toMinutes(hours).toInt() + duration.value = TimestampUtils.durationToString(hours.toInt(), remainMinutes) + showDuration.value = minutes > 0 + + val organizerAddress = conferenceInfo.organizer + if (organizerAddress != null) { + val contact = coreContext.contactsManager.findContactByAddress(organizerAddress) + organizer.value = if (contact != null) + contact.fullName + else + LinphoneUtils.getDisplayName(conferenceInfo.organizer) + } else { + Log.e("[Scheduled Conference] No organizer SIP URI found for: ${conferenceInfo.uri?.asStringUriOnly()}") + } + + computeParticipantsLists() + } + + fun destroy() {} + + fun delete() { + Log.w("[Scheduled Conference] Deleting conference info with URI: ${conferenceInfo.uri?.asStringUriOnly()}") + coreContext.core.deleteConferenceInformation(conferenceInfo) + } + + fun toggleExpand() { + expanded.value = expanded.value == false + } + + private fun computeParticipantsLists() { + var participantsListShort = "" + var participantsListExpanded = "" + + for (participant in conferenceInfo.participants) { + val contact = coreContext.contactsManager.findContactByAddress(participant) + val name = if (contact != null) contact.fullName else LinphoneUtils.getDisplayName(participant) + val address = participant.asStringUriOnly() + participantsListShort += "$name, " + participantsListExpanded += "$name ($address)\n" + } + participantsListShort = participantsListShort.dropLast(2) + participantsListExpanded = participantsListExpanded.dropLast(1) + + participantsShort.value = participantsListShort + participantsExpanded.value = participantsListExpanded + } +} diff --git a/app/src/main/java/org/linphone/activities/main/conference/data/TimeZoneData.kt b/app/src/main/java/org/linphone/activities/main/conference/data/TimeZoneData.kt new file mode 100644 index 000000000..de9007e53 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/conference/data/TimeZoneData.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.main.conference.data + +import java.util.* +import java.util.concurrent.TimeUnit +import kotlin.math.abs + +class TimeZoneData(timeZone: TimeZone) : Comparable { + val id: String = timeZone.id + private val hours: Long + private val minutes: Long + private val gmt: String + + init { + hours = TimeUnit.MILLISECONDS.toHours(timeZone.rawOffset.toLong()) + minutes = abs( + TimeUnit.MILLISECONDS.toMinutes(timeZone.rawOffset.toLong()) - + TimeUnit.HOURS.toMinutes(hours) + ) + + gmt = if (hours > 0) { + String.format("%s - GMT+%d:%02d", timeZone.id, hours, minutes) + } else { + String.format("%s - GMT%d:%02d", timeZone.id, hours, minutes) + } + } + + override fun toString(): String { + return gmt + } + + override fun compareTo(other: TimeZoneData): Int { + if (hours == other.hours) { + if (minutes == other.minutes) { + return id.compareTo(other.id) + } + return minutes.compareTo(other.minutes) + } + return hours.compareTo(other.hours) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingFragment.kt b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingFragment.kt new file mode 100644 index 000000000..23e1fb610 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingFragment.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.main.conference.fragments + +import android.os.Bundle +import android.text.format.DateFormat.is24HourFormat +import android.view.View +import androidx.navigation.navGraphViewModels +import com.google.android.material.datepicker.CalendarConstraints +import com.google.android.material.datepicker.DateValidatorPointForward +import com.google.android.material.datepicker.MaterialDatePicker +import com.google.android.material.timepicker.MaterialTimePicker +import com.google.android.material.timepicker.TimeFormat +import org.linphone.R +import org.linphone.activities.GenericFragment +import org.linphone.activities.main.conference.viewmodels.ConferenceSchedulingViewModel +import org.linphone.activities.navigateToParticipantsList +import org.linphone.databinding.ConferenceSchedulingFragmentBinding + +class ConferenceSchedulingFragment : GenericFragment() { + private val viewModel: ConferenceSchedulingViewModel by navGraphViewModels(R.id.conference_scheduling_nav_graph) + + override fun getLayoutId(): Int = R.layout.conference_scheduling_fragment + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.lifecycleOwner = viewLifecycleOwner + + binding.viewModel = viewModel + + binding.setBackClickListener { + goBack() + } + + binding.setNextClickListener { + navigateToParticipantsList() + } + + binding.setDatePickerClickListener { + val constraintsBuilder = + CalendarConstraints.Builder() + .setValidator(DateValidatorPointForward.now()) + val picker = + MaterialDatePicker.Builder.datePicker() + .setCalendarConstraints(constraintsBuilder.build()) + .setTitleText(R.string.conference_schedule_date) + .setSelection(viewModel.dateTimestamp) + .build() + picker.addOnPositiveButtonClickListener { + val selection = picker.selection + if (selection != null) { + viewModel.setDate(selection) + } + } + picker.show(requireFragmentManager(), "Date picker") + } + + binding.setTimePickerClickListener { + val isSystem24Hour = is24HourFormat(requireContext()) + val clockFormat = if (isSystem24Hour) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H + val picker = + MaterialTimePicker.Builder() + .setTimeFormat(clockFormat) + .setTitleText(R.string.conference_schedule_time) + .setHour(viewModel.hour) + .setMinute(viewModel.minutes) + .build() + picker.addOnPositiveButtonClickListener { + viewModel.setTime(picker.hour, picker.minute) + } + picker.show(requireFragmentManager(), "Time picker") + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingParticipantsListFragment.kt b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingParticipantsListFragment.kt new file mode 100644 index 000000000..6f565c116 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingParticipantsListFragment.kt @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.main.conference.fragments + +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.View +import androidx.navigation.navGraphViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.GenericFragment +import org.linphone.activities.main.conference.viewmodels.ConferenceSchedulingViewModel +import org.linphone.activities.navigateToSummary +import org.linphone.contact.ContactsSelectionAdapter +import org.linphone.core.tools.Log +import org.linphone.databinding.ConferenceSchedulingParticipantsListFragmentBinding +import org.linphone.utils.AppUtils +import org.linphone.utils.PermissionHelper + +class ConferenceSchedulingParticipantsListFragment : GenericFragment() { + private val viewModel: ConferenceSchedulingViewModel by navGraphViewModels(R.id.conference_scheduling_nav_graph) + private lateinit var adapter: ContactsSelectionAdapter + + override fun getLayoutId(): Int = R.layout.conference_scheduling_participants_list_fragment + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.lifecycleOwner = viewLifecycleOwner + + binding.viewModel = viewModel + + adapter = ContactsSelectionAdapter(viewLifecycleOwner) + adapter.setLimeCapabilityRequired(viewModel.isEncrypted.value == true) + binding.contactsList.adapter = adapter + + val layoutManager = LinearLayoutManager(activity) + binding.contactsList.layoutManager = layoutManager + + // Divider between items + binding.contactsList.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager)) + + binding.setBackClickListener { + goBack() + } + + binding.setNextClickListener { + navigateToSummary() + } + + viewModel.contactsList.observe( + viewLifecycleOwner + ) { + adapter.submitList(it) + } + viewModel.sipContactsSelected.observe( + viewLifecycleOwner + ) { + viewModel.updateContactsList() + } + + viewModel.selectedAddresses.observe( + viewLifecycleOwner + ) { + adapter.updateSelectedAddresses(it) + } + viewModel.filter.observe( + viewLifecycleOwner + ) { + viewModel.applyFilter() + } + + adapter.selectedContact.observe( + viewLifecycleOwner + ) { + it.consume { searchResult -> + viewModel.toggleSelectionForSearchResult(searchResult) + } + } + + if (!PermissionHelper.get().hasReadContactsPermission()) { + Log.i("[Conference Creation] Asking for READ_CONTACTS permission") + requestPermissions(arrayOf(android.Manifest.permission.READ_CONTACTS), 0) + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + if (requestCode == 0) { + val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED + if (granted) { + Log.i("[Conference Creation] READ_CONTACTS permission granted") + coreContext.contactsManager.onReadContactsPermissionGranted() + coreContext.contactsManager.fetchContactsAsync() + } else { + Log.w("[Conference Creation] READ_CONTACTS permission denied") + } + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingSummaryFragment.kt b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingSummaryFragment.kt new file mode 100644 index 000000000..4d253e0f6 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingSummaryFragment.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.main.conference.fragments + +import android.os.Bundle +import android.view.View +import androidx.navigation.navGraphViewModels +import org.linphone.R +import org.linphone.activities.GenericFragment +import org.linphone.activities.goToScheduledConferences +import org.linphone.activities.main.MainActivity +import org.linphone.activities.main.conference.viewmodels.ConferenceSchedulingViewModel +import org.linphone.activities.navigateToConferenceWaitingRoom +import org.linphone.databinding.ConferenceSchedulingSummaryFragmentBinding + +class ConferenceSchedulingSummaryFragment : GenericFragment() { + private val viewModel: ConferenceSchedulingViewModel by navGraphViewModels(R.id.conference_scheduling_nav_graph) + + override fun getLayoutId(): Int = R.layout.conference_scheduling_summary_fragment + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.lifecycleOwner = viewLifecycleOwner + + binding.viewModel = viewModel + + binding.setBackClickListener { + goBack() + } + + binding.setCreateConferenceClickListener { + viewModel.createConference() + } + + viewModel.conferenceCreationCompletedEvent.observe( + viewLifecycleOwner + ) { + it.consume { pair -> + if (viewModel.scheduleForLater.value == true) { + (requireActivity() as MainActivity).showSnackBar(R.string.conference_schedule_info_created) + goToScheduledConferences() + } else { + navigateToConferenceWaitingRoom(pair.first, pair.second) + } + } + } + + viewModel.onMessageToNotifyEvent.observe( + viewLifecycleOwner + ) { + it.consume { messageId -> + (activity as MainActivity).showSnackBar(messageId) + } + } + + viewModel.computeParticipantsData() + } +} diff --git a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceWaitingRoomFragment.kt b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceWaitingRoomFragment.kt new file mode 100644 index 000000000..869b4e95f --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceWaitingRoomFragment.kt @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.main.conference.fragments + +import android.Manifest +import android.annotation.TargetApi +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.GenericFragment +import org.linphone.activities.main.conference.viewmodels.ConferenceWaitingRoomViewModel +import org.linphone.activities.navigateToDialer +import org.linphone.core.tools.Log +import org.linphone.databinding.ConferenceWaitingRoomFragmentBinding +import org.linphone.mediastream.Version +import org.linphone.utils.PermissionHelper + +class ConferenceWaitingRoomFragment : GenericFragment() { + private lateinit var viewModel: ConferenceWaitingRoomViewModel + + override fun getLayoutId(): Int = R.layout.conference_waiting_room_fragment + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.lifecycleOwner = viewLifecycleOwner + + viewModel = ViewModelProvider( + this + )[ConferenceWaitingRoomViewModel::class.java] + binding.viewModel = viewModel + + val conferenceSubject = arguments?.getString("Subject") + viewModel.subject.value = conferenceSubject + + viewModel.cancelConferenceJoiningEvent.observe( + viewLifecycleOwner + ) { + it.consume { + if (viewModel.joinInProgress.value == true) { + val conferenceUri = arguments?.getString("Address") + val callToCancel = coreContext.core.calls.find { call -> + call.remoteAddress.asStringUriOnly() == conferenceUri + } + if (callToCancel != null) { + Log.i("[Conference Waiting Room] Call to conference server with URI [$conferenceUri] was started, terminate it") + callToCancel.terminate() + } else { + Log.w("[Conference Waiting Room] Call to conference server with URI [$conferenceUri] wasn't found!") + } + } + navigateToDialer() + } + } + + viewModel.joinConferenceEvent.observe( + viewLifecycleOwner + ) { + it.consume { callParams -> + val conferenceUri = arguments?.getString("Address") + if (conferenceUri != null) { + val conferenceAddress = coreContext.core.interpretUrl(conferenceUri) + if (conferenceAddress != null) { + Log.i("[Conference Waiting Room] Calling conference SIP URI: ${conferenceAddress.asStringUriOnly()}") + coreContext.startCall(conferenceAddress, callParams) + } else { + Log.e("[Conference Waiting Room] Failed to parse conference SIP URI: $conferenceUri") + } + } else { + Log.e("[Conference Waiting Room] Failed to find conference SIP URI in arguments") + } + } + } + + viewModel.askPermissionEvent.observe( + viewLifecycleOwner + ) { + it.consume { permission -> + Log.i("[Conference Waiting Room] Asking for $permission permission") + requestPermissions(arrayOf(permission), 0) + } + } + + viewModel.leaveWaitingRoomEvent.observe( + viewLifecycleOwner + ) { + it.consume { + goBack() + } + } + + if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) { + checkPermissions() + } + } + + override fun onResume() { + super.onResume() + + coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface + } + + override fun onPause() { + coreContext.core.nativePreviewWindowId = null + + super.onPause() + } + + @TargetApi(Version.API23_MARSHMALLOW_60) + private fun checkPermissions() { + val permissionsRequiredList = arrayListOf() + if (!PermissionHelper.get().hasRecordAudioPermission()) { + Log.i("[Conference Waiting Room] Asking for RECORD_AUDIO permission") + permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO) + } + if (!PermissionHelper.get().hasCameraPermission()) { + Log.i("[Conference Waiting Room] Asking for CAMERA permission") + permissionsRequiredList.add(Manifest.permission.CAMERA) + } + if (permissionsRequiredList.isNotEmpty()) { + val permissionsRequired = arrayOfNulls(permissionsRequiredList.size) + permissionsRequiredList.toArray(permissionsRequired) + requestPermissions(permissionsRequired, 0) + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + if (requestCode == 0) { + for (i in permissions.indices) { + when (permissions[i]) { + Manifest.permission.RECORD_AUDIO -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { + Log.i("[Conference Waiting Room] RECORD_AUDIO permission has been granted") + viewModel.enableMic() + } + Manifest.permission.CAMERA -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { + Log.i("[Conference Waiting Room] CAMERA permission has been granted") + coreContext.core.reloadVideoDevices() + viewModel.enableVideo() + } + } + } + } + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/conference/fragments/ScheduledConferencesFragment.kt b/app/src/main/java/org/linphone/activities/main/conference/fragments/ScheduledConferencesFragment.kt new file mode 100644 index 000000000..1f74142c7 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/conference/fragments/ScheduledConferencesFragment.kt @@ -0,0 +1,137 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.main.conference.fragments + +import android.app.Dialog +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import org.linphone.R +import org.linphone.activities.GenericFragment +import org.linphone.activities.main.MainActivity +import org.linphone.activities.main.conference.adapters.ScheduledConferencesAdapter +import org.linphone.activities.main.conference.viewmodels.ScheduledConferencesViewModel +import org.linphone.activities.main.viewmodels.DialogViewModel +import org.linphone.activities.navigateToConferenceScheduling +import org.linphone.activities.navigateToConferenceWaitingRoom +import org.linphone.databinding.ConferencesScheduledFragmentBinding +import org.linphone.utils.AppUtils +import org.linphone.utils.DialogUtils +import org.linphone.utils.RecyclerViewHeaderDecoration + +class ScheduledConferencesFragment : GenericFragment() { + private lateinit var viewModel: ScheduledConferencesViewModel + private lateinit var adapter: ScheduledConferencesAdapter + + override fun getLayoutId(): Int = R.layout.conferences_scheduled_fragment + + private var deleteConferenceInfoDialog: Dialog? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.lifecycleOwner = viewLifecycleOwner + + viewModel = ViewModelProvider( + this + )[ScheduledConferencesViewModel::class.java] + binding.viewModel = viewModel + + adapter = ScheduledConferencesAdapter( + viewLifecycleOwner + ) + binding.conferenceInfoList.adapter = adapter + + val layoutManager = LinearLayoutManager(activity) + binding.conferenceInfoList.layoutManager = layoutManager + + // Displays date header + val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter) + binding.conferenceInfoList.addItemDecoration(headerItemDecoration) + + viewModel.conferences.observe( + viewLifecycleOwner + ) { + adapter.submitList(it) + } + + adapter.copyAddressToClipboardEvent.observe( + viewLifecycleOwner + ) { + it.consume { address -> + val clipboard = + requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("Conference address", address.asStringUriOnly()) + clipboard.setPrimaryClip(clip) + + (activity as MainActivity).showSnackBar(R.string.conference_schedule_address_copied_to_clipboard) + } + } + + adapter.joinConferenceEvent.observe( + viewLifecycleOwner + ) { + it.consume { pair -> + navigateToConferenceWaitingRoom(pair.first, pair.second) + } + } + + adapter.deleteConferenceInfoEvent.observe( + viewLifecycleOwner + ) { + it.consume { data -> + val dialogViewModel = + DialogViewModel(AppUtils.getString(R.string.conference_info_confirm_removal)) + deleteConferenceInfoDialog = + DialogUtils.getVoipDialog(requireContext(), dialogViewModel) + + dialogViewModel.showCancelButton( + { + deleteConferenceInfoDialog?.dismiss() + }, + getString(R.string.dialog_cancel) + ) + + dialogViewModel.showDeleteButton( + { + viewModel.deleteConferenceInfo(data) + deleteConferenceInfoDialog?.dismiss() + (requireActivity() as MainActivity).showSnackBar(R.string.conference_info_removed) + }, + getString(R.string.dialog_delete) + ) + + deleteConferenceInfoDialog?.show() + } + } + + binding.setBackClickListener { + goBack() + } + + binding.setNewConferenceClickListener { + navigateToConferenceScheduling() + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceSchedulingViewModel.kt b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceSchedulingViewModel.kt new file mode 100644 index 000000000..2f5ca4a79 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceSchedulingViewModel.kt @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.main.conference.viewmodels + +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import java.util.* +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.main.conference.data.ConferenceSchedulingParticipantData +import org.linphone.activities.main.conference.data.Duration +import org.linphone.activities.main.conference.data.TimeZoneData +import org.linphone.contact.ContactsSelectionViewModel +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.utils.Event +import org.linphone.utils.TimestampUtils + +class ConferenceSchedulingViewModel : ContactsSelectionViewModel() { + val subject = MutableLiveData() + val description = MutableLiveData() + + val scheduleForLater = MutableLiveData() + + val formattedDate = MutableLiveData() + val formattedTime = MutableLiveData() + + val isEncrypted = MutableLiveData() + + val sendInviteViaChat = MutableLiveData() + val sendInviteViaEmail = MutableLiveData() + + val participantsData = MutableLiveData>() + + val address = MutableLiveData
() + + val conferenceCreationInProgress = MutableLiveData() + + val conferenceCreationCompletedEvent: MutableLiveData>> by lazy { + MutableLiveData>>() + } + + val continueEnabled: MediatorLiveData = MediatorLiveData() + + var timeZone = MutableLiveData() + val timeZones: List = computeTimeZonesList() + + var duration = MutableLiveData() + val durationList: List = computeDurationList() + + var dateTimestamp: Long = System.currentTimeMillis() + var hour: Int = 0 + var minutes: Int = 0 + + private val conferenceScheduler = coreContext.core.createConferenceScheduler() + + private val chatRoomListener = object : ChatRoomListenerStub() { + override fun onStateChanged(room: ChatRoom, state: ChatRoom.State) { + if (state == ChatRoom.State.Created) { + Log.i("[Conference Creation] Chat room created") + room.removeListener(this) + } else if (state == ChatRoom.State.CreationFailed) { + Log.e("[Conference Creation] Group chat room creation has failed !") + room.removeListener(this) + } + } + } + + private val listener = object : ConferenceSchedulerListenerStub() { + override fun onStateChanged( + conferenceScheduler: ConferenceScheduler, + state: ConferenceSchedulerState + ) { + Log.i("[Conference Creation] Conference scheduler state is $state") + if (state == ConferenceSchedulerState.Ready) { + val conferenceAddress = conferenceScheduler.info?.uri + Log.i("[Conference Creation] Conference info created, address will be ${conferenceAddress?.asStringUriOnly()}") + conferenceAddress ?: return + + address.value = conferenceAddress!! + + if (sendInviteViaChat.value == true) { + // Send conference info even when conf is not scheduled for later + // as the conference server doesn't invite participants automatically + val chatRoomParams = coreContext.core.createDefaultChatRoomParams() + chatRoomParams.backend = ChatRoomBackend.FlexisipChat + chatRoomParams.isGroupEnabled = false + chatRoomParams.isEncryptionEnabled = true + chatRoomParams.subject = subject.value + conferenceScheduler.sendInvitations(chatRoomParams) + } else { + conferenceCreationInProgress.value = false + conferenceCreationCompletedEvent.value = Event(Pair(conferenceAddress.asStringUriOnly(), conferenceScheduler.info?.subject)) + } + } + } + + override fun onInvitationsSent( + conferenceScheduler: ConferenceScheduler, + failedInvitations: Array? + ) { + Log.i("[Conference Creation] Conference information successfully sent to all participants") + conferenceCreationInProgress.value = false + + if (failedInvitations?.isNotEmpty() == true) { + for (address in failedInvitations) { + Log.e("[Conference Creation] Conference information wasn't sent to participant ${address.asStringUriOnly()}") + } + onMessageToNotifyEvent.value = Event(R.string.conference_schedule_info_not_sent_to_participant) + } + + val conferenceAddress = conferenceScheduler.info?.uri + if (conferenceAddress == null) { + Log.e("[Conference Creation] Conference address is null!") + } else { + conferenceCreationCompletedEvent.value = Event(Pair(conferenceAddress.asStringUriOnly(), conferenceScheduler.info?.subject)) + } + } + } + + init { + sipContactsSelected.value = true + + subject.value = "" + scheduleForLater.value = false + isEncrypted.value = false + sendInviteViaChat.value = true + sendInviteViaEmail.value = false + + timeZone.value = timeZones.find { + it.id == TimeZone.getDefault().id + } + duration.value = durationList.find { + it.value == 3600 + } + + continueEnabled.value = false + continueEnabled.addSource(subject) { + continueEnabled.value = allMandatoryFieldsFilled() + } + continueEnabled.addSource(scheduleForLater) { + continueEnabled.value = allMandatoryFieldsFilled() + } + continueEnabled.addSource(formattedDate) { + continueEnabled.value = allMandatoryFieldsFilled() + } + continueEnabled.addSource(formattedTime) { + continueEnabled.value = allMandatoryFieldsFilled() + } + + conferenceScheduler.addListener(listener) + } + + override fun onCleared() { + conferenceScheduler.removeListener(listener) + participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy) + + super.onCleared() + } + + fun toggleSchedule() { + scheduleForLater.value = scheduleForLater.value == false + } + + fun setDate(d: Long) { + dateTimestamp = d + formattedDate.value = TimestampUtils.dateToString(dateTimestamp, false) + } + + fun setTime(h: Int, m: Int) { + hour = h + minutes = m + formattedTime.value = TimestampUtils.timeToString(hour, minutes) + } + + fun updateEncryption(enable: Boolean) { + isEncrypted.value = enable + } + + fun computeParticipantsData() { + participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy) + val list = arrayListOf() + + for (address in selectedAddresses.value.orEmpty()) { + val data = ConferenceSchedulingParticipantData(address, isEncrypted.value == true) + list.add(data) + } + + participantsData.value = list + } + + fun createConference() { + val participantsCount = selectedAddresses.value.orEmpty().size + if (participantsCount == 0) { + Log.e("[Conference Creation] Couldn't create conference without any participant!") + return + } + + conferenceCreationInProgress.value = true + val core = coreContext.core + val participants = arrayOfNulls
(selectedAddresses.value.orEmpty().size) + selectedAddresses.value?.toArray(participants) + val localAddress = core.defaultAccount?.params?.identityAddress + + // TODO: Temporary workaround for chat room, to be removed once we can get matching chat room from conference + /*val chatRoomParams = core.createDefaultChatRoomParams() + chatRoomParams.backend = ChatRoomBackend.FlexisipChat + chatRoomParams.enableGroup(true) + chatRoomParams.subject = subject.value + val chatRoom = core.createChatRoom(chatRoomParams, localAddress, participants) + if (chatRoom == null) { + Log.e("[Conference Creation] Failed to create a chat room with same subject & participants as for conference") + } else { + Log.i("[Conference Creation] Creating chat room with same subject [${subject.value}] & participants as for conference") + chatRoom.addListener(chatRoomListener) + }*/ + // END OF TODO + + val conferenceInfo = Factory.instance().createConferenceInfo() + conferenceInfo.organizer = localAddress + conferenceInfo.subject = subject.value + conferenceInfo.description = description.value + conferenceInfo.setParticipants(participants) + if (scheduleForLater.value == true) { + val startTime = getConferenceStartTimestamp() + conferenceInfo.dateTime = startTime + val duration = duration.value?.value ?: 0 + conferenceInfo.duration = duration + } + conferenceScheduler.info = conferenceInfo // Will trigger the conference creation automatically + } + + private fun computeTimeZonesList(): List { + return TimeZone.getAvailableIDs().map { id -> TimeZoneData(TimeZone.getTimeZone(id)) }.toList().sorted() + } + + private fun computeDurationList(): List { + // Duration value is in minutes as according to conferenceInfo.setDuration() doc + return arrayListOf(Duration(30, "30min"), Duration(60, "1h"), Duration(120, "2h")) + } + + private fun allMandatoryFieldsFilled(): Boolean { + return !subject.value.isNullOrEmpty() && + ( + scheduleForLater.value == false || + ( + !formattedDate.value.isNullOrEmpty() && + !formattedTime.value.isNullOrEmpty() + ) + ) + } + + private fun getConferenceStartTimestamp(): Long { + val calendar = Calendar.getInstance(TimeZone.getTimeZone(timeZone.value?.id ?: TimeZone.getDefault().id)) + calendar.timeInMillis = dateTimestamp + calendar.set(Calendar.HOUR_OF_DAY, hour) + calendar.set(Calendar.MINUTE, minutes) + return calendar.timeInMillis / 1000 // Linphone expects a time_t (so in seconds) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceWaitingRoomViewModel.kt b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceWaitingRoomViewModel.kt new file mode 100644 index 000000000..eeed7e054 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceWaitingRoomViewModel.kt @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.main.conference.viewmodels + +import android.Manifest +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.utils.AudioRouteUtils +import org.linphone.utils.Event +import org.linphone.utils.PermissionHelper + +class ConferenceWaitingRoomViewModel : ViewModel() { + val subject = MutableLiveData() + + val isMicrophoneMuted = MutableLiveData() + + val audioRoutesEnabled = MutableLiveData() + + val audioRoutesSelected = MutableLiveData() + + val isSpeakerSelected = MutableLiveData() + + val isBluetoothHeadsetSelected = MutableLiveData() + + val isVideoAvailable = MutableLiveData() + + val isVideoEnabled = MutableLiveData() + + val isSwitchCameraAvailable = MutableLiveData() + + val joinInProgress = MutableLiveData() + + val askPermissionEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val cancelConferenceJoiningEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val joinConferenceEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val leaveWaitingRoomEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + private val callParams: CallParams = coreContext.core.createCallParams(null)!! + + private val listener: CoreListenerStub = object : CoreListenerStub() { + override fun onAudioDevicesListUpdated(core: Core) { + Log.i("[Conference Waiting Room] Audio devices list updated") + onAudioDevicesListUpdated() + } + + override fun onCallStateChanged( + core: Core, + call: Call, + state: Call.State?, + message: String + ) { + if (state == Call.State.Connected) { + Log.i("[Conference Waiting Room] Call is now connected, leaving waiting room fragment") + leaveWaitingRoomEvent.value = Event(true) + } + } + } + + init { + val core = coreContext.core + core.addListener(listener) + + callParams.isMicEnabled = PermissionHelper.get().hasRecordAudioPermission() + Log.i("[Conference Waiting Room] Microphone will be ${if (callParams.isMicEnabled) "enabled" else "muted"}") + updateMicState() + + isVideoAvailable.value = core.isVideoCaptureEnabled || core.isVideoPreviewEnabled + callParams.isVideoEnabled = core.videoActivationPolicy.automaticallyInitiate + Log.i("[Conference Waiting Room] Video will be ${if (callParams.isVideoEnabled) "enabled" else "disabled"}") + updateVideoState() + + if (AudioRouteUtils.isBluetoothAudioRouteAvailable()) { + setBluetoothAudioRoute() + } else if (isVideoAvailable.value == true && isVideoEnabled.value == true) { + setSpeakerAudioRoute() + } else { + setEarpieceAudioRoute() + } + updateAudioRouteState() + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + + super.onCleared() + } + + fun cancel() { + cancelConferenceJoiningEvent.value = Event(true) + } + + fun start() { + joinInProgress.value = true + joinConferenceEvent.value = Event(callParams) + } + + fun toggleMuteMicrophone() { + if (!PermissionHelper.get().hasRecordAudioPermission()) { + askPermissionEvent.value = Event(Manifest.permission.RECORD_AUDIO) + return + } + + callParams.isMicEnabled = !callParams.isMicEnabled + Log.i("[Conference Waiting Room] Microphone will be ${if (callParams.isMicEnabled) "enabled" else "muted"}") + updateMicState() + } + + fun enableMic() { + Log.i("[Conference Waiting Room] Microphone will be enabled") + callParams.isMicEnabled = true + updateMicState() + } + + fun toggleSpeaker() { + if (isSpeakerSelected.value == true) { + setEarpieceAudioRoute() + } else { + setSpeakerAudioRoute() + } + } + + fun toggleAudioRoutesMenu() { + audioRoutesSelected.value = audioRoutesSelected.value != true + } + + fun setBluetoothAudioRoute() { + Log.i("[Conference Waiting Room] Set default output audio device to Bluetooth") + callParams.outputAudioDevice = coreContext.core.audioDevices.find { + it.type == AudioDevice.Type.Bluetooth && it.hasCapability(AudioDevice.Capabilities.CapabilityPlay) + } + callParams.inputAudioDevice = coreContext.core.audioDevices.find { + it.type == AudioDevice.Type.Bluetooth && it.hasCapability(AudioDevice.Capabilities.CapabilityRecord) + } + updateAudioRouteState() + } + + fun setSpeakerAudioRoute() { + Log.i("[Conference Waiting Room] Set default output audio device to Speaker") + callParams.outputAudioDevice = coreContext.core.audioDevices.find { + it.type == AudioDevice.Type.Speaker && it.hasCapability(AudioDevice.Capabilities.CapabilityPlay) + } + callParams.inputAudioDevice = coreContext.core.audioDevices.find { + it.type == AudioDevice.Type.Microphone && it.hasCapability(AudioDevice.Capabilities.CapabilityRecord) + } + updateAudioRouteState() + } + + fun setEarpieceAudioRoute() { + Log.i("[Conference Waiting Room] Set default output audio device to Earpiece") + callParams.outputAudioDevice = coreContext.core.audioDevices.find { + it.type == AudioDevice.Type.Earpiece && it.hasCapability(AudioDevice.Capabilities.CapabilityPlay) + } + callParams.inputAudioDevice = coreContext.core.audioDevices.find { + it.type == AudioDevice.Type.Microphone && it.hasCapability(AudioDevice.Capabilities.CapabilityRecord) + } + updateAudioRouteState() + } + + fun toggleVideo() { + if (!PermissionHelper.get().hasCameraPermission()) { + askPermissionEvent.value = Event(Manifest.permission.CAMERA) + return + } + callParams.isVideoEnabled = !callParams.isVideoEnabled + Log.i("[Conference Waiting Room] Video will be ${if (callParams.isVideoEnabled) "enabled" else "disabled"}") + updateVideoState() + } + + fun enableVideo() { + Log.i("[Conference Waiting Room] Video will be enabled") + callParams.isVideoEnabled = true + updateVideoState() + } + + fun switchCamera() { + Log.i("[Conference Waiting Room] Switching camera") + coreContext.switchCamera() + } + + private fun updateMicState() { + isMicrophoneMuted.value = !callParams.isMicEnabled + } + + private fun onAudioDevicesListUpdated() { + val bluetoothDeviceAvailable = AudioRouteUtils.isBluetoothAudioRouteAvailable() + audioRoutesEnabled.value = bluetoothDeviceAvailable + + if (!bluetoothDeviceAvailable) { + audioRoutesSelected.value = false + Log.w("[Conference Waiting Room] Bluetooth device no longer available, switching back to default microphone & earpiece/speaker") + if (isBluetoothHeadsetSelected.value == true) { + for (audioDevice in coreContext.core.audioDevices) { + if (isVideoEnabled.value == true) { + if (audioDevice.type == AudioDevice.Type.Speaker) { + callParams.outputAudioDevice = audioDevice + } + } else { + if (audioDevice.type == AudioDevice.Type.Earpiece) { + callParams.outputAudioDevice = audioDevice + } + } + if (audioDevice.type == AudioDevice.Type.Microphone) { + callParams.inputAudioDevice = audioDevice + } + } + } + } + + updateAudioRouteState() + } + + private fun updateAudioRouteState() { + val outputDeviceType = callParams.outputAudioDevice?.type + isSpeakerSelected.value = outputDeviceType == AudioDevice.Type.Speaker + isBluetoothHeadsetSelected.value = outputDeviceType == AudioDevice.Type.Bluetooth + } + + private fun updateVideoState() { + isVideoEnabled.value = callParams.isVideoEnabled + isSwitchCameraAvailable.value = callParams.isVideoEnabled && coreContext.showSwitchCameraButton() + coreContext.core.isVideoPreviewEnabled = callParams.isVideoEnabled + } +} diff --git a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ScheduledConferencesViewModel.kt b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ScheduledConferencesViewModel.kt new file mode 100644 index 000000000..5e2355d44 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ScheduledConferencesViewModel.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.main.conference.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.activities.main.conference.data.ScheduledConferenceData +import org.linphone.core.ConferenceInfo +import org.linphone.core.Core +import org.linphone.core.CoreListenerStub +import org.linphone.core.tools.Log + +class ScheduledConferencesViewModel : ViewModel() { + val conferences = MutableLiveData>() + + private val listener = object : CoreListenerStub() { + override fun onConferenceInfoReceived(core: Core, conferenceInfo: ConferenceInfo) { + Log.i("[Scheduled Conferences] New conference info received") + val conferencesList = arrayListOf() + conferencesList.addAll(conferences.value.orEmpty()) + val data = ScheduledConferenceData(conferenceInfo) + conferencesList.add(data) + conferences.value = conferencesList + } + } + + init { + coreContext.core.addListener(listener) + computeConferenceInfoList() + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + conferences.value.orEmpty().forEach(ScheduledConferenceData::destroy) + super.onCleared() + } + + fun deleteConferenceInfo(data: ScheduledConferenceData) { + val conferenceInfoList = arrayListOf() + + conferenceInfoList.addAll(conferences.value.orEmpty()) + conferenceInfoList.remove(data) + + data.delete() + data.destroy() + conferences.value = conferenceInfoList + } + + private fun computeConferenceInfoList() { + conferences.value.orEmpty().forEach(ScheduledConferenceData::destroy) + + val conferencesList = arrayListOf() + + val now = System.currentTimeMillis() / 1000 // Linphone uses time_t in seconds + val oneHourAgo = now - 3600 // Show all conferences from 1 hour ago and forward + for (conferenceInfo in coreContext.core.getConferenceInformationListAfterTime(oneHourAgo)) { + val data = ScheduledConferenceData(conferenceInfo) + conferencesList.add(data) + } + + conferences.value = conferencesList + Log.i("[Scheduled Conferences] Found ${conferencesList.size} future conferences") + } +} diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt index ac738abd4..552245bf6 100644 --- a/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt @@ -138,7 +138,7 @@ class ContactEditorFragment : GenericFragment(), S Log.i("[Contact Editor] WRITE_CONTACTS permission granted") } else { Log.w("[Contact Editor] WRITE_CONTACTS permission denied") - (requireActivity() as MainActivity).showSnackBar(R.string.contact_editor_write_permission_denied) + (activity as MainActivity).showSnackBar(R.string.contact_editor_write_permission_denied) goBack() } } diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt index cf7384fae..dff171a33 100644 --- a/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt @@ -71,7 +71,6 @@ class DetailContactFragment : GenericFragment() { val contact = sharedViewModel.selectedContact.value if (contact == null) { Log.e("[Contact] Contact is null, aborting!") - // (activity as MainActivity).showSnackBar(R.string.error) goBack() return } @@ -140,7 +139,7 @@ class DetailContactFragment : GenericFragment() { confirmContactRemoval() } - viewModel.onErrorEvent.observe( + viewModel.onMessageToNotifyEvent.observe( viewLifecycleOwner ) { it.consume { messageResourceId -> diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt index bc419400f..540dad03c 100644 --- a/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt @@ -138,7 +138,6 @@ class MasterContactsFragment : MasterFragment = MutableLiveData() override val displayName: MutableLiveData = MutableLiveData() override val securityLevel: MutableLiveData = MutableLiveData() @@ -91,7 +91,7 @@ class ContactViewModel(val contactInternal: Contact) : ErrorReportingViewModel() } else if (state == ChatRoom.State.CreationFailed) { Log.e("[Contact Detail] Group chat room creation has failed !") waitForChatRoomCreation.value = false - onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack) + onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack) } } } @@ -115,7 +115,7 @@ class ContactViewModel(val contactInternal: Contact) : ErrorReportingViewModel() } else { waitForChatRoomCreation.value = false Log.e("[Contact Detail] Couldn't create chat room with address $address") - onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack) + onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack) } } diff --git a/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt b/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt index 04791ca56..de60f66b9 100644 --- a/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt @@ -31,6 +31,7 @@ import android.content.res.Configuration import android.net.Uri import android.os.Bundle import android.view.View +import android.widget.Toast import androidx.lifecycle.ViewModelProvider import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.transition.MaterialSharedAxis @@ -43,6 +44,7 @@ import org.linphone.activities.main.dialer.viewmodels.DialerViewModel import org.linphone.activities.main.fragments.SecureFragment import org.linphone.activities.main.viewmodels.DialogViewModel import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.activities.navigateToConferenceScheduling import org.linphone.activities.navigateToConfigFileViewer import org.linphone.activities.navigateToContacts import org.linphone.compatibility.Compatibility @@ -103,11 +105,17 @@ class DialerFragment : SecureFragment() { navigateToContacts(viewModel.enteredUri.value) } + binding.setNewConferenceClickListener { + sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.conferenceSchedulingFragment) + navigateToConferenceScheduling() + } + binding.setTransferCallClickListener { if (viewModel.transferCall()) { // Transfer has been consumed, otherwise it might have been a "bis" use sharedViewModel.pendingCallTransfer = false viewModel.transferVisibility.value = false + coreContext.onCallStarted() } } @@ -147,6 +155,14 @@ class DialerFragment : SecureFragment() { } } + viewModel.onMessageToNotifyEvent.observe( + viewLifecycleOwner + ) { + it.consume { id -> + Toast.makeText(requireContext(), id, Toast.LENGTH_SHORT).show() + } + } + if (corePreferences.firstStart) { Log.w("[Dialer] First start detected, wait for assistant to be finished to check for update & request permissions") return diff --git a/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt b/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt index 964399d6b..45874eb05 100644 --- a/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt @@ -47,6 +47,8 @@ class DialerViewModel : LogsUploadViewModel() { val autoInitiateVideoCalls = MutableLiveData() + val scheduleConferenceAvailable = MutableLiveData() + val updateAvailableEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -136,6 +138,7 @@ class DialerViewModel : LogsUploadViewModel() { transferVisibility.value = false showSwitchCamera.value = coreContext.showSwitchCameraButton() + scheduleConferenceAvailable.value = LinphoneUtils.isRemoteConferencingAvailable() } override fun onCleared() { @@ -195,7 +198,13 @@ class DialerViewModel : LogsUploadViewModel() { fun transferCall(): Boolean { val addressToCall = enteredUri.value.orEmpty() return if (addressToCall.isNotEmpty()) { - coreContext.transferCallTo(addressToCall) + onMessageToNotifyEvent.value = Event( + if (coreContext.transferCallTo(addressToCall)) { + org.linphone.R.string.dialer_transfer_succeded + } else { + org.linphone.R.string.dialer_transfer_failed + } + ) eraseAll() true } else { diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/AudioViewerFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/AudioViewerFragment.kt index 1f6877079..8dd25f62d 100644 --- a/app/src/main/java/org/linphone/activities/main/files/fragments/AudioViewerFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/files/fragments/AudioViewerFragment.kt @@ -48,7 +48,6 @@ class AudioViewerFragment : GenericViewerFragment : SecureFragment() val content = sharedViewModel.contentToOpen.value if (content == null) { Log.e("[Generic Viewer] Content is null, aborting!") - // (activity as MainActivity).showSnackBar(R.string.error) findNavController().navigateUp() return } diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/ImageViewerFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/ImageViewerFragment.kt index daae3d5aa..2bab58d08 100644 --- a/app/src/main/java/org/linphone/activities/main/files/fragments/ImageViewerFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/files/fragments/ImageViewerFragment.kt @@ -44,7 +44,6 @@ class ImageViewerFragment : GenericViewerFragment() val content = sharedViewModel.contentToOpen.value if (content == null) { Log.e("[PDF Viewer] Content is null, aborting!") - // (activity as MainActivity).showSnackBar(R.string.error) findNavController().navigateUp() return } diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/TextViewerFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/TextViewerFragment.kt index ea81e9124..470ce50c1 100644 --- a/app/src/main/java/org/linphone/activities/main/files/fragments/TextViewerFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/files/fragments/TextViewerFragment.kt @@ -42,7 +42,6 @@ class TextViewerFragment : GenericViewerFragment( val content = sharedViewModel.contentToOpen.value if (content == null) { Log.e("[Text Viewer] Content is null, aborting!") - // (activity as MainActivity).showSnackBar(R.string.error) findNavController().navigateUp() return } diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/VideoViewerFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/VideoViewerFragment.kt index f0efad396..f82711429 100644 --- a/app/src/main/java/org/linphone/activities/main/files/fragments/VideoViewerFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/files/fragments/VideoViewerFragment.kt @@ -46,7 +46,6 @@ class VideoViewerFragment : GenericViewerFragment. - */ -package org.linphone.activities.main.history.data - -import java.text.SimpleDateFormat -import java.util.* -import org.linphone.R -import org.linphone.contact.GenericContactData -import org.linphone.core.Call -import org.linphone.core.CallLog -import org.linphone.utils.TimestampUtils - -class CallLogData(callLog: CallLog) : GenericContactData(callLog.remoteAddress) { - val statusIconResource: Int by lazy { - if (callLog.dir == Call.Dir.Incoming) { - if (callLog.status == Call.Status.Missed) { - R.drawable.call_status_missed - } else { - R.drawable.call_status_incoming - } - } else { - R.drawable.call_status_outgoing - } - } - - val iconContentDescription: Int by lazy { - if (callLog.dir == Call.Dir.Incoming) { - if (callLog.status == Call.Status.Missed) { - R.string.content_description_missed_call - } else { - R.string.content_description_incoming_call - } - } else { - R.string.content_description_outgoing_call - } - } - - val directionIconResource: Int by lazy { - if (callLog.dir == Call.Dir.Incoming) { - if (callLog.status == Call.Status.Missed) { - R.drawable.call_missed - } else { - R.drawable.call_incoming - } - } else { - R.drawable.call_outgoing - } - } - - val duration: String by lazy { - val dateFormat = SimpleDateFormat(if (callLog.duration >= 3600) "HH:mm:ss" else "mm:ss", Locale.getDefault()) - val cal = Calendar.getInstance() - cal[0, 0, 0, 0, 0] = callLog.duration - dateFormat.format(cal.time) - } - - val date: String by lazy { - TimestampUtils.toString(callLog.startDate, shortDate = false, hideYear = false) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/history/data/GroupedCallLogData.kt b/app/src/main/java/org/linphone/activities/main/history/data/GroupedCallLogData.kt index ea76e8fe8..e00a9c5dd 100644 --- a/app/src/main/java/org/linphone/activities/main/history/data/GroupedCallLogData.kt +++ b/app/src/main/java/org/linphone/activities/main/history/data/GroupedCallLogData.kt @@ -19,14 +19,18 @@ */ package org.linphone.activities.main.history.data +import org.linphone.activities.main.history.viewmodels.CallLogViewModel import org.linphone.core.CallLog class GroupedCallLogData(callLog: CallLog) { var lastCallLog: CallLog = callLog val callLogs = arrayListOf(callLog) - val lastCallLogData = CallLogData(lastCallLog) + val lastCallLogViewModel: CallLogViewModel + get() { + return CallLogViewModel(lastCallLog) + } fun destroy() { - lastCallLogData.destroy() + lastCallLogViewModel.destroy() } } diff --git a/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt index 825921125..f444e86e2 100644 --- a/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt @@ -57,7 +57,6 @@ class DetailCallLogFragment : GenericFragment() { val callLogGroup = sharedViewModel.selectedCallLogGroup.value if (callLogGroup == null) { Log.e("[History] Call log group is null, aborting!") - // (activity as MainActivity).showSnackBar(R.string.error) findNavController().navigateUp() return } @@ -70,7 +69,7 @@ class DetailCallLogFragment : GenericFragment() { useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false - viewModel.addRelatedCallLogs(callLogGroup.callLogs) + viewModel.relatedCallLogs.value = callLogGroup.callLogs binding.setBackClickListener { goBack() @@ -134,7 +133,7 @@ class DetailCallLogFragment : GenericFragment() { } } - viewModel.onErrorEvent.observe( + viewModel.onMessageToNotifyEvent.observe( viewLifecycleOwner ) { it.consume { messageResourceId -> diff --git a/app/src/main/java/org/linphone/activities/main/history/fragments/DetailConferenceCallLogFragment.kt b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailConferenceCallLogFragment.kt new file mode 100644 index 000000000..efcd9b5fe --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailConferenceCallLogFragment.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2010-2020 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.main.history.fragments + +import android.os.Bundle +import android.view.View +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.fragment.findNavController +import org.linphone.R +import org.linphone.activities.* +import org.linphone.activities.main.* +import org.linphone.activities.main.history.viewmodels.CallLogViewModel +import org.linphone.activities.main.history.viewmodels.CallLogViewModelFactory +import org.linphone.activities.main.viewmodels.SharedMainViewModel +import org.linphone.core.tools.Log +import org.linphone.databinding.HistoryConfDetailFragmentBinding +import org.linphone.utils.Event + +class DetailConferenceCallLogFragment : GenericFragment() { + private lateinit var viewModel: CallLogViewModel + private lateinit var sharedViewModel: SharedMainViewModel + + override fun getLayoutId(): Int = R.layout.history_conf_detail_fragment + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.lifecycleOwner = viewLifecycleOwner + + sharedViewModel = requireActivity().run { + ViewModelProvider(this)[SharedMainViewModel::class.java] + } + binding.sharedMainViewModel = sharedViewModel + + val callLogGroup = sharedViewModel.selectedCallLogGroup.value + if (callLogGroup == null) { + Log.e("[History] Call log group is null, aborting!") + findNavController().navigateUp() + return + } + + viewModel = ViewModelProvider( + this, + CallLogViewModelFactory(callLogGroup.lastCallLog) + )[CallLogViewModel::class.java] + binding.viewModel = viewModel + + useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false + + viewModel.relatedCallLogs.value = callLogGroup.callLogs + + binding.setBackClickListener { + goBack() + } + + viewModel.onMessageToNotifyEvent.observe( + viewLifecycleOwner + ) { + it.consume { messageResourceId -> + (activity as MainActivity).showSnackBar(messageResourceId) + } + } + } + + override fun goBack() { + if (sharedViewModel.isSlidingPaneSlideable.value == true) { + sharedViewModel.closeSlidingPaneEvent.value = Event(true) + } else { + navigateToEmptyCallHistory() + } + } +} diff --git a/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt b/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt index bbd2e0cfa..389a74be9 100644 --- a/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt @@ -35,6 +35,7 @@ import com.google.android.material.transition.MaterialSharedAxis import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R +import org.linphone.activities.* import org.linphone.activities.clearDisplayedCallHistory import org.linphone.activities.main.fragments.MasterFragment import org.linphone.activities.main.history.adapters.CallLogsListAdapter @@ -44,6 +45,7 @@ import org.linphone.activities.main.viewmodels.DialogViewModel import org.linphone.activities.main.viewmodels.SharedMainViewModel import org.linphone.activities.main.viewmodels.TabsViewModel import org.linphone.activities.navigateToCallHistory +import org.linphone.activities.navigateToConferenceCallHistory import org.linphone.activities.navigateToDialer import org.linphone.core.tools.Log import org.linphone.databinding.HistoryMasterFragmentBinding @@ -125,7 +127,6 @@ class MasterCallLogsFragment : MasterFragment - if (listViewModel.missedCallLogsSelected.value == false) { - adapter.submitList(callLogs) - } - } - - listViewModel.missedCallLogs.observe( - viewLifecycleOwner - ) { callLogs -> - if (listViewModel.missedCallLogsSelected.value == true) { - adapter.submitList(callLogs) - } - } - - listViewModel.missedCallLogsSelected.observe( - viewLifecycleOwner - ) { - if (it) { - adapter.submitList(listViewModel.missedCallLogs.value) - } else { - adapter.submitList(listViewModel.callLogs.value) - } + adapter.submitList(callLogs) } listViewModel.contactsUpdatedEvent.observe( @@ -230,24 +211,29 @@ class MasterCallLogsFragment : MasterFragment sharedViewModel.selectedCallLogGroup.value = callLog - navigateToCallHistory(binding.slidingPane) + if (callLog.lastCallLog.wasConference()) { + navigateToConferenceCallHistory(binding.slidingPane) + } else { + navigateToCallHistory(binding.slidingPane) + } } } adapter.startCallToEvent.observe( - viewLifecycleOwner + viewLifecycleOwner, ) { it.consume { callLogGroup -> val remoteAddress = callLogGroup.lastCallLog.remoteAddress - if (coreContext.core.callsNb > 0) { + val conferenceInfo = coreContext.core.findConferenceInformationFromUri(remoteAddress) + if (conferenceInfo != null) { + navigateToConferenceWaitingRoom(remoteAddress.asStringUriOnly(), conferenceInfo.subject) + } else if (coreContext.core.callsNb > 0) { Log.i("[History] Starting dialer with pre-filled URI ${remoteAddress.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}") - sharedViewModel.updateDialerAnimationsBasedOnDestination.value = - Event(R.id.masterCallLogsFragment) + sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.masterCallLogsFragment) val args = Bundle() args.putString("URI", remoteAddress.asStringUriOnly()) args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer) - // If auto start call setting is enabled, ignore it - args.putBoolean("SkipAutoCallStart", true) + args.putBoolean("SkipAutoCallStart", true) // If auto start call setting is enabled, ignore it navigateToDialer(args) } else { val localAddress = callLogGroup.lastCallLog.localAddress @@ -256,13 +242,6 @@ class MasterCallLogsFragment : MasterFragment= 3600) "HH:mm:ss" else "mm:ss", Locale.getDefault()) + val cal = Calendar.getInstance() + cal[0, 0, 0, 0, 0] = callLog.duration + dateFormat.format(cal.time) + } + + val date: String by lazy { + TimestampUtils.toString(callLog.startDate, shortDate = false, hideYear = false) + } + val startCallEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -61,17 +112,30 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r val secureChatAllowed = contact.value?.friend?.getPresenceModelForUriOrTel(peerSipUri)?.hasCapability(FriendCapability.LimeX3Dh) ?: false - val relatedCallLogs = MutableLiveData>() + val relatedCallLogs = MutableLiveData>() private val listener = object : CoreListenerStub() { override fun onCallLogUpdated(core: Core, log: CallLog) { if (callLog.remoteAddress.weakEqual(log.remoteAddress) && callLog.localAddress.weakEqual(log.localAddress)) { Log.i("[History Detail] New call log for ${callLog.remoteAddress.asStringUriOnly()} with local address ${callLog.localAddress.asStringUriOnly()}") - addRelatedCallLogs(arrayListOf(log)) + val list = arrayListOf() + list.add(callLog) + list.addAll(relatedCallLogs.value.orEmpty()) + relatedCallLogs.value = list } } } + val isConferenceCallLog = callLog.wasConference() + + val conferenceSubject = callLog.conferenceInfo?.subject + val conferenceParticipantsData = MutableLiveData>() + val conferenceTime = MutableLiveData() + val conferenceDate = MutableLiveData() + + override val showGroupChatAvatar: Boolean + get() = isConferenceCallLog + private val chatRoomListener = object : ChatRoomListenerStub() { override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) { if (state == ChatRoom.State.Created) { @@ -80,7 +144,7 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r } else if (state == ChatRoom.State.CreationFailed) { Log.e("[History Detail] Group chat room creation has failed !") waitForChatRoomCreation.value = false - onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack) + onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack) } } } @@ -89,17 +153,31 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r waitForChatRoomCreation.value = false coreContext.core.addListener(listener) + + val conferenceInfo = callLog.conferenceInfo + if (conferenceInfo != null) { + conferenceTime.value = TimestampUtils.timeToString(conferenceInfo.dateTime) + conferenceDate.value = if (TimestampUtils.isToday(conferenceInfo.dateTime)) { + AppUtils.getString(R.string.today) + } else { + TimestampUtils.toString(conferenceInfo.dateTime, onlyDate = true, shortDate = false, hideYear = false) + } + val list = arrayListOf() + 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) { - val callsHistory = ArrayList() - - // 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 - } } diff --git a/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogsListViewModel.kt b/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogsListViewModel.kt index a14fcc859..a4c179fff 100644 --- a/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogsListViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogsListViewModel.kt @@ -31,15 +31,20 @@ import org.linphone.utils.LinphoneUtils import org.linphone.utils.TimestampUtils class CallLogsListViewModel : ViewModel() { - val callLogs = MutableLiveData>() - val missedCallLogs = MutableLiveData>() + val displayedCallLogs = MutableLiveData>() - val missedCallLogsSelected = MutableLiveData() + val filter = MutableLiveData() + + val showConferencesFilter = MutableLiveData() val contactsUpdatedEvent: MutableLiveData> by lazy { MutableLiveData>() } + private val callLogs = MutableLiveData>() + private val missedCallLogs = MutableLiveData>() + private val conferenceCallLogs = MutableLiveData>() + 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,61 +120,69 @@ class CallLogsListViewModel : ViewModel() { updateCallLogs() } - private fun updateCallLogs() { - callLogs.value.orEmpty().forEach(GroupedCallLogData::destroy) - missedCallLogs.value.orEmpty().forEach(GroupedCallLogData::destroy) - - val list = arrayListOf() - val missedList = arrayListOf() - + private fun computeCallLogs(callLogs: Array, missed: Boolean, conference: Boolean): ArrayList { var previousCallLogGroup: GroupedCallLogData? = null - var previousMissedCallLogGroup: GroupedCallLogData? = null - for (callLog in coreContext.core.callLogs) { - if (previousCallLogGroup == null) { - previousCallLogGroup = GroupedCallLogData(callLog) - } else if (previousCallLogGroup.lastCallLog.localAddress.weakEqual(callLog.localAddress) && - previousCallLogGroup.lastCallLog.remoteAddress.weakEqual(callLog.remoteAddress) - ) { - if (TimestampUtils.isSameDay(previousCallLogGroup.lastCallLog.startDate, callLog.startDate)) { - previousCallLogGroup.callLogs.add(callLog) - previousCallLogGroup.lastCallLog = callLog + val list = arrayListOf() + + 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.equal(callLog.remoteAddress) + ) { + if (TimestampUtils.isSameDay( + previousCallLogGroup.lastCallLog.startDate, + callLog.startDate + ) + ) { + previousCallLogGroup.callLogs.add(callLog) + previousCallLogGroup.lastCallLog = callLog + } else { + list.add(previousCallLogGroup) + previousCallLogGroup = GroupedCallLogData(callLog) + } } else { list.add(previousCallLogGroup) previousCallLogGroup = GroupedCallLogData(callLog) } - } else { - 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 + } + + 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() + + when (filter.value) { + CallLogsFilter.MISSED -> displayedList.addAll(missedCallLogs.value.orEmpty()) + CallLogsFilter.CONFERENCE -> displayedList.addAll(conferenceCallLogs.value.orEmpty()) + else -> displayedList.addAll(callLogs.value.orEmpty()) } - callLogs.value = list - missedCallLogs.value = missedList + displayedCallLogs.value = displayedList } } + +enum class CallLogsFilter { + ALL, + MISSED, + CONFERENCE +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt index cb18d3a35..1bd80f1fc 100644 --- a/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt @@ -46,7 +46,6 @@ class AccountSettingsFragment : GenericSettingFragment. */ -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() { - private lateinit var viewModel: StatisticsListViewModel +class ConferencesSettingsFragment : GenericSettingFragment() { + 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() + } } } diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/SettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/SettingsFragment.kt index 6395507b9..14c4fc816 100644 --- a/app/src/main/java/org/linphone/activities/main/settings/fragments/SettingsFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/SettingsFragment.kt @@ -177,5 +177,11 @@ class SettingsFragment : SecureFragment() { navigateToAdvancedSettings(binding.slidingPane) } } + + viewModel.conferencesSettingsListener = object : SettingListenerStub() { + override fun onClicked() { + navigateToConferencesSettings(binding.slidingPane) + } + } } } diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AccountSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AccountSettingsViewModel.kt index 81760768b..4c5012741 100644 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AccountSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AccountSettingsViewModel.kt @@ -390,6 +390,27 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel( } val linkPhoneNumberEvent = MutableLiveData>() + 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() + + 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() + 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() { diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt index f95ab222e..b52997f90 100644 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt @@ -105,13 +105,6 @@ class CallSettingsViewModel : GenericSettingsViewModel() { } val api26OrHigher = MutableLiveData() - val fullScreenListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.fullScreenCallUI = newValue - } - } - val fullScreen = MutableLiveData() - val overlayListener = object : SettingListenerStub() { override fun onBoolValueChanged(newValue: Boolean) { prefs.showCallOverlay = newValue @@ -151,6 +144,13 @@ class CallSettingsViewModel : GenericSettingsViewModel() { } val autoStartCallRecording = MutableLiveData() + val remoteCallRecordingListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + core.isRecordAwareEnabled = newValue + } + } + val remoteCallRecording = MutableLiveData() + 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 diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ConferencesSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ConferencesSettingsViewModel.kt new file mode 100644 index 000000000..52d1c005c --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ConferencesSettingsViewModel.kt @@ -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 . + */ +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() + val layoutLabels = MutableLiveData>() + private val layoutValues = arrayListOf() + + init { + initLayoutsList() + } + + private fun initLayoutsList() { + val labels = arrayListOf() + + 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()) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/SettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/SettingsViewModel.kt index 258ebed0d..74bf954dd 100644 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/SettingsViewModel.kt @@ -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>() @@ -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() diff --git a/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt b/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt index 346d6d3d7..fc35e9a18 100644 --- a/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt @@ -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() { navigateToAbout() } + binding.setConferencesClickListener { + sharedViewModel.toggleDrawerEvent.value = Event(true) + navigateToScheduledConferences() + } + binding.setQuitClickListener { Log.i("[Side Menu] Quitting app") requireActivity().finishAndRemoveTask() diff --git a/app/src/main/java/org/linphone/activities/main/sidemenu/viewmodels/SideMenuViewModel.kt b/app/src/main/java/org/linphone/activities/main/sidemenu/viewmodels/SideMenuViewModel.kt index 9ed09c824..9fdce5da2 100644 --- a/app/src/main/java/org/linphone/activities/main/sidemenu/viewmodels/SideMenuViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/sidemenu/viewmodels/SideMenuViewModel.kt @@ -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 diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/CallOverlayViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/CallOverlayViewModel.kt index 88807c6fa..79863593f 100644 --- a/app/src/main/java/org/linphone/activities/main/viewmodels/CallOverlayViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/viewmodels/CallOverlayViewModel.kt @@ -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() @@ -34,17 +35,19 @@ 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) { - removeCallOverlay() - } } } + + override fun onLastCallEnded(core: Core) { + Log.i("[Call Overlay] Last call ended, removing it") + removeCallOverlay() + } } init { diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/LogsUploadViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/LogsUploadViewModel.kt index 337938289..ca9a083df 100644 --- a/app/src/main/java/org/linphone/activities/main/viewmodels/LogsUploadViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/viewmodels/LogsUploadViewModel.kt @@ -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() val resetCompleteEvent: MutableLiveData> by lazy { diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/ErrorReportingViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/MessageNotifierViewModel.kt similarity index 83% rename from app/src/main/java/org/linphone/activities/main/viewmodels/ErrorReportingViewModel.kt rename to app/src/main/java/org/linphone/activities/main/viewmodels/MessageNotifierViewModel.kt index 24c7a3924..6e81ee8db 100644 --- a/app/src/main/java/org/linphone/activities/main/viewmodels/ErrorReportingViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/viewmodels/MessageNotifierViewModel.kt @@ -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>() +/* Helper for view models to notify user of a massage through a Snackbar */ +abstract class MessageNotifierViewModel : ViewModel() { + val onMessageToNotifyEvent = MutableLiveData>() } diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/TabsViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/TabsViewModel.kt index e7fb0162b..d6578e55d 100644 --- a/app/src/main/java/org/linphone/activities/main/viewmodels/TabsViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/viewmodels/TabsViewModel.kt @@ -101,7 +101,6 @@ class TabsViewModel : ViewModel() { } override fun onCleared() { - if (corePreferences.enableAnimations) bounceAnimator.end() coreContext.core.removeListener(listener) super.onCleared() } diff --git a/app/src/main/java/org/linphone/activities/voip/CallActivity.kt b/app/src/main/java/org/linphone/activities/voip/CallActivity.kt new file mode 100644 index 000000000..70a4f8fb2 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/CallActivity.kt @@ -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 . + */ +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() + 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(permissionsRequiredList.size) + permissionsRequiredList.toArray(permissionsRequired) + requestPermissions(permissionsRequired, 0) + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + if (requestCode == 0) { + for (i in permissions.indices) { + when (permissions[i]) { + Manifest.permission.RECORD_AUDIO -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { + Log.i("[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)*/ + } +} diff --git a/app/src/main/java/org/linphone/activities/voip/data/CallData.kt b/app/src/main/java/org/linphone/activities/voip/data/CallData.kt new file mode 100644 index 000000000..588438589 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/data/CallData.kt @@ -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 . + */ +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() + val isRemotelyPaused = MutableLiveData() + val canBePaused = MutableLiveData() + + val isRecording = MutableLiveData() + val isRemotelyRecorded = MutableLiveData() + + val isInRemoteConference = MutableLiveData() + val remoteConferenceSubject = MutableLiveData() + val isActiveAndNotInConference = MediatorLiveData() + + val isOutgoing = MutableLiveData() + val isIncoming = MutableLiveData() + + 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
(conference.participantCount) + val addresses = arrayListOf
() + 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 + } +} diff --git a/app/src/main/java/org/linphone/activities/call/data/CallStatisticsData.kt b/app/src/main/java/org/linphone/activities/voip/data/CallStatisticsData.kt similarity index 93% rename from app/src/main/java/org/linphone/activities/call/data/CallStatisticsData.kt rename to app/src/main/java/org/linphone/activities/voip/data/CallStatisticsData.kt index bbfe7a887..b742a910d 100644 --- a/app/src/main/java/org/linphone/activities/call/data/CallStatisticsData.kt +++ b/app/src/main/java/org/linphone/activities/voip/data/CallStatisticsData.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2020 Belledonne Communications SARL. + * Copyright (c) 2010-2021 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.activities.call.data +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() - val isExpanded = MutableLiveData() - 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() audioList.add(StatItemData(StatType.CAPTURE)) diff --git a/app/src/main/java/org/linphone/activities/voip/data/ConferenceParticipantData.kt b/app/src/main/java/org/linphone/activities/voip/data/ConferenceParticipantData.kt new file mode 100644 index 000000000..ae05689b2 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/data/ConferenceParticipantData.kt @@ -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 . + */ +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() + val isMeAdmin = MutableLiveData() + + 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") + } + } +} diff --git a/app/src/main/java/org/linphone/activities/voip/data/ConferenceParticipantDeviceData.kt b/app/src/main/java/org/linphone/activities/voip/data/ConferenceParticipantDeviceData.kt new file mode 100644 index 000000000..762f58b48 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/data/ConferenceParticipantDeviceData.kt @@ -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 . + */ +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() + + val activeSpeaker = MutableLiveData() + + val isInConference = MutableLiveData() + + 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 + } + } +} diff --git a/app/src/main/java/org/linphone/activities/call/data/StatItemData.kt b/app/src/main/java/org/linphone/activities/voip/data/StatItemData.kt similarity index 97% rename from app/src/main/java/org/linphone/activities/call/data/StatItemData.kt rename to app/src/main/java/org/linphone/activities/voip/data/StatItemData.kt index d37cd9dfd..67e4a18eb 100644 --- a/app/src/main/java/org/linphone/activities/call/data/StatItemData.kt +++ b/app/src/main/java/org/linphone/activities/voip/data/StatItemData.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2020 Belledonne Communications SARL. + * Copyright (c) 2010-2021 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.activities.call.data +package org.linphone.activities.voip.data import androidx.lifecycle.MutableLiveData import java.text.DecimalFormat diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/ActiveCallOrConferenceFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/ActiveCallOrConferenceFragment.kt new file mode 100644 index 000000000..c61f22a40 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/fragments/ActiveCallOrConferenceFragment.kt @@ -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 . + */ +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() { + 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(R.id.conference_active_speaker_layout) + val window = + layout?.findViewById(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(R.id.remote_layout) + val window = + layout?.findViewById(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(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(R.id.remote_layout) + val remoteVideoView = remoteLayout.findViewById(R.id.remote_video_surface) + coreContext.core.nativeVideoWindowId = remoteVideoView + val localVideoView = remoteLayout.findViewById(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(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(inflated) + binding?.lifecycleOwner = viewLifecycleOwner + startTimer(R.id.grid_conference_timer) + } + + binding.stubbedAudioRoutes.setOnInflateListener { _, inflated -> + val binding = DataBindingUtil.bind(inflated) + binding?.lifecycleOwner = viewLifecycleOwner + } + + binding.stubbedNumpad.setOnInflateListener { _, inflated -> + val binding = DataBindingUtil.bind(inflated) + binding?.lifecycleOwner = viewLifecycleOwner + } + + binding.stubbedCallStats.setOnInflateListener { _, inflated -> + val binding = DataBindingUtil.bind(inflated) + binding?.lifecycleOwner = viewLifecycleOwner + } + + binding.stubbedPausedCall.setOnInflateListener { _, inflated -> + val binding = DataBindingUtil.bind(inflated) + binding?.lifecycleOwner = viewLifecycleOwner + } + + binding.stubbedRemotelyPausedCall.setOnInflateListener { _, inflated -> + val binding = DataBindingUtil.bind(inflated) + binding?.lifecycleOwner = viewLifecycleOwner + } + + binding.stubbedPausedConference.setOnInflateListener { _, inflated -> + val binding = DataBindingUtil.bind(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() + } +} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/CallsListFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/CallsListFragment.kt new file mode 100644 index 000000000..899263c61 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/fragments/CallsListFragment.kt @@ -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 . + */ +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() { + 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) + } +} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/ChatFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/ChatFragment.kt new file mode 100644 index 000000000..0d953d058 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/fragments/ChatFragment.kt @@ -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 . + */ +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() { + 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, + 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() + + 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()) + ) + + startActivityForResult(chooserIntent, 0) + } +} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceAddParticipantsFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceAddParticipantsFragment.kt new file mode 100644 index 000000000..f3690e09d --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceAddParticipantsFragment.kt @@ -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 . + */ +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() { + 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, + 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") + } + } + } +} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceLayoutFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceLayoutFragment.kt new file mode 100644 index 000000000..ae01dcb17 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceLayoutFragment.kt @@ -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 . + */ +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() { + 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(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(R.id.too_many_participants_dialog) + dialog?.visibility = View.VISIBLE + } +} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceParticipantsFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceParticipantsFragment.kt new file mode 100644 index 000000000..d8badadea --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceParticipantsFragment.kt @@ -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 . + */ +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() { + 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 + } + } +} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/IncomingCallFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/IncomingCallFragment.kt new file mode 100644 index 000000000..2cfca960b --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/fragments/IncomingCallFragment.kt @@ -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 . + */ +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() { + 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(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 + } + } +} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/OutgoingCallFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/OutgoingCallFragment.kt new file mode 100644 index 000000000..9f2ab7365 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/fragments/OutgoingCallFragment.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.voip.fragments + +import android.os.Bundle +import android.os.SystemClock +import android.view.View +import android.widget.Chronometer +import androidx.databinding.DataBindingUtil +import androidx.databinding.ViewDataBinding +import androidx.navigation.navGraphViewModels +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.databinding.VoipCallOutgoingFragmentBinding + +class OutgoingCallFragment : GenericFragment() { + 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_outgoing_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(R.id.outgoing_call_timer) + timer.base = + SystemClock.elapsedRealtime() - (1000 * it.call.duration) // Linphone timestamps are in seconds + timer.start() + } + } + + binding.stubNumpad.setOnInflateListener { _, inflated -> + val binding = DataBindingUtil.bind(inflated) + binding?.lifecycleOwner = viewLifecycleOwner + } + } +} diff --git a/app/src/main/java/org/linphone/activities/call/fragments/StatusFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/StatusFragment.kt similarity index 84% rename from app/src/main/java/org/linphone/activities/call/fragments/StatusFragment.kt rename to app/src/main/java/org/linphone/activities/voip/fragments/StatusFragment.kt index 7837b7482..e9f8f748d 100644 --- a/app/src/main/java/org/linphone/activities/call/fragments/StatusFragment.kt +++ b/app/src/main/java/org/linphone/activities/voip/fragments/StatusFragment.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2020 Belledonne Communications SARL. + * Copyright (c) 2010-2021 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,30 +17,31 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.activities.call.fragments +package org.linphone.activities.voip.fragments import android.app.Dialog import android.os.Bundle import android.view.View import androidx.lifecycle.ViewModelProvider +import androidx.navigation.navGraphViewModels import java.util.* import org.linphone.R import org.linphone.activities.GenericFragment -import org.linphone.activities.call.viewmodels.SharedCallViewModel -import org.linphone.activities.call.viewmodels.StatusViewModel import org.linphone.activities.main.viewmodels.DialogViewModel +import org.linphone.activities.voip.viewmodels.ControlsViewModel +import org.linphone.activities.voip.viewmodels.StatusViewModel import org.linphone.core.Call import org.linphone.core.tools.Log -import org.linphone.databinding.CallStatusFragmentBinding +import org.linphone.databinding.VoipStatusFragmentBinding import org.linphone.utils.DialogUtils -import org.linphone.utils.Event -class StatusFragment : GenericFragment() { +class StatusFragment : GenericFragment() { private lateinit var viewModel: StatusViewModel - private lateinit var sharedViewModel: SharedCallViewModel + private val controlsViewModel: ControlsViewModel by navGraphViewModels(R.id.call_nav_graph) + private var zrtpDialog: Dialog? = null - override fun getLayoutId(): Int = R.layout.call_status_fragment + override fun getLayoutId(): Int = R.layout.voip_status_fragment override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -51,14 +52,6 @@ class StatusFragment : GenericFragment() { viewModel = ViewModelProvider(this)[StatusViewModel::class.java] binding.viewModel = viewModel - sharedViewModel = requireActivity().run { - ViewModelProvider(this)[SharedCallViewModel::class.java] - } - - binding.setStatsClickListener { - sharedViewModel.toggleDrawerEvent.value = Event(true) - } - binding.setRefreshClickListener { viewModel.refreshRegister() } @@ -72,6 +65,14 @@ class StatusFragment : GenericFragment() { } } } + + viewModel.showCallStatsEvent.observe( + viewLifecycleOwner + ) { + it.consume { + controlsViewModel.showCallStats() + } + } } override fun onCreate(savedInstanceState: Bundle?) { diff --git a/app/src/main/java/org/linphone/activities/voip/viewmodels/CallsViewModel.kt b/app/src/main/java/org/linphone/activities/voip/viewmodels/CallsViewModel.kt new file mode 100644 index 000000000..ec565c8c8 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/viewmodels/CallsViewModel.kt @@ -0,0 +1,265 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.voip.viewmodels + +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R +import org.linphone.activities.voip.data.CallData +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.utils.AppUtils +import org.linphone.utils.Event + +class CallsViewModel : ViewModel() { + val currentCallData = MutableLiveData() + + val callsData = MutableLiveData>() + + val inactiveCallsCount = MutableLiveData() + + val currentCallUnreadChatMessageCount = MutableLiveData() + + val chatAndCallsCount = MediatorLiveData() + + val callConnectedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val callEndedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val callUpdateEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val noMoreCallEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + private val listener = object : CoreListenerStub() { + override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) { + updateUnreadChatCount() + } + + override fun onMessageReceived(core: Core, chatRoom: ChatRoom, message: ChatMessage) { + updateUnreadChatCount() + } + + override fun onLastCallEnded(core: Core) { + currentCallData.value?.destroy() + noMoreCallEvent.value = Event(true) + } + + override fun onCallStateChanged(core: Core, call: Call, state: Call.State, message: String) { + Log.i("[Calls] Call with ID [${call.callLog.callId}] state changed: $state") + + if (state == Call.State.IncomingEarlyMedia || state == Call.State.IncomingReceived || state == Call.State.OutgoingInit) { + if (!callDataAlreadyExists(call)) { + addCallToList(call) + } + } + + val currentCall = core.currentCall + if (currentCall != null && currentCallData.value?.call != currentCall) { + updateCurrentCallData(currentCall) + } else if (currentCall == null && core.callsNb > 0) { + updateCurrentCallData(currentCall) + } + + if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) { + removeCallFromList(call) + if (core.callsNb > 0) { + callEndedEvent.value = Event(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.Connected) { + callConnectedEvent.value = Event(call) + } else if (state == Call.State.StreamsRunning) { + callUpdateEvent.value = Event(call) + } + + updateInactiveCallsCount() + } + } + + init { + coreContext.core.addListener(listener) + + val currentCall = coreContext.core.currentCall + if (currentCall != null) { + currentCallData.value?.destroy() + + val viewModel = CallData(currentCall) + currentCallData.value = viewModel + } + + chatAndCallsCount.value = 0 + chatAndCallsCount.addSource(inactiveCallsCount) { + chatAndCallsCount.value = updateCallsAndChatCount() + } + chatAndCallsCount.addSource(currentCallUnreadChatMessageCount) { + chatAndCallsCount.value = updateCallsAndChatCount() + } + + initCallList() + updateInactiveCallsCount() + updateUnreadChatCount() + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + + currentCallData.value?.destroy() + callsData.value.orEmpty().forEach(CallData::destroy) + + super.onCleared() + } + + fun mergeCallsIntoConference() { + Log.i("[Calls] Merging all calls into new conference") + val core = coreContext.core + val params = core.createConferenceParams(null) + params.subject = AppUtils.getString(R.string.conference_local_title) + val conference = core.createConferenceWithParams(params) + conference?.addParticipants(core.calls) + } + + private fun initCallList() { + val calls = arrayListOf() + + for (call in coreContext.core.calls) { + val data: CallData = if (currentCallData.value?.call == call) { + currentCallData.value!! + } else { + CallData(call) + } + Log.i("[Calls] Adding call with ID ${call.callLog.callId} to calls list") + calls.add(data) + } + + callsData.value = calls + } + + private fun addCallToList(call: Call) { + Log.i("[Calls] Adding call with ID ${call.callLog.callId} to calls list") + + val calls = arrayListOf() + calls.addAll(callsData.value.orEmpty()) + + val data = CallData(call) + calls.add(data) + + callsData.value = calls + } + + private fun removeCallFromList(call: Call) { + Log.i("[Calls] Removing call with ID ${call.callLog.callId} from calls list") + + val calls = arrayListOf() + calls.addAll(callsData.value.orEmpty()) + + val data = calls.find { it.call == call } + if (data == null) { + Log.w("[Calls] Data for call to remove wasn't found") + } else { + data.destroy() + calls.remove(data) + } + + callsData.value = calls + } + + private fun updateCurrentCallData(currentCall: Call?) { + var callToUse = currentCall + if (currentCall == null) { + Log.w("[Calls] Current call is now null") + + val firstCall = coreContext.core.calls.find { call -> + call.state != Call.State.Error && call.state != Call.State.End && call.state != Call.State.Released + } + if (firstCall != null && currentCallData.value?.call != firstCall) { + Log.i("[Calls] Using [${firstCall.remoteAddress.asStringUriOnly()}] call as \"current\" call") + callToUse = firstCall + } + } + + if (callToUse == null) { + Log.w("[Calls] No call found to be used as \"current\"") + return + } + + var found = false + for (callData in callsData.value.orEmpty()) { + if (callData.call == callToUse) { + Log.i("[Calls] Updating current call to: ${callData.call.remoteAddress.asStringUriOnly()}") + currentCallData.value = callData + found = true + break + } + } + if (!found) { + Log.w("[Calls] Call with ID [${callToUse.callLog.callId}] not found in calls data list, shouldn't happen!") + val viewModel = CallData(callToUse) + currentCallData.value = viewModel + } + + // updateUnreadChatCount() + } + + private fun callDataAlreadyExists(call: Call): Boolean { + for (callData in callsData.value.orEmpty()) { + if (callData.call == call) { + return true + } + } + return false + } + + private fun updateCallsAndChatCount(): Int { + return (inactiveCallsCount.value ?: 0) + (currentCallUnreadChatMessageCount.value ?: 0) + } + + private fun updateUnreadChatCount() { + // For now we don't display in-call chat, so use global unread chat messages count + currentCallUnreadChatMessageCount.value = coreContext.core.unreadChatMessageCountFromActiveLocals + } + + private fun updateInactiveCallsCount() { + // TODO: handle local conference + inactiveCallsCount.value = coreContext.core.callsNb - 1 + } +} diff --git a/app/src/main/java/org/linphone/activities/voip/viewmodels/ConferenceParticipantsViewModel.kt b/app/src/main/java/org/linphone/activities/voip/viewmodels/ConferenceParticipantsViewModel.kt new file mode 100644 index 000000000..69f178007 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/viewmodels/ConferenceParticipantsViewModel.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.voip.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import org.linphone.contact.ContactsSelectionViewModel +import org.linphone.core.Address +import org.linphone.core.Conference +import org.linphone.core.tools.Log + +class ConferenceParticipantsViewModelFactory(private val conference: Conference) : + ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return ConferenceParticipantsViewModel(conference) as T + } +} + +class ConferenceParticipantsViewModel(val conference: Conference) : ContactsSelectionViewModel() { + init { + selectCurrentParticipants() + } + + fun applyChanges() { + // Adding new participants first, because if we remove all of them (or all of them except one) + // It will terminate the conference first and we won't be able to add new participants after + for (address in selectedAddresses.value.orEmpty()) { + val participant = conference.participantList.find { participant -> + participant.address.weakEqual(address) + } + if (participant == null) { + Log.i("[Conference Participants] Participant ${address.asStringUriOnly()} will be added to group") + conference.addParticipant(address) + } + } + + // Removing participants + for (participant in conference.participantList) { + val member = selectedAddresses.value.orEmpty().find { address -> + participant.address.weakEqual(address) + } + if (member == null) { + Log.w("[Conference Participants] Participant ${participant.address.asStringUriOnly()} will be removed from conference") + conference.removeParticipant(participant) + } + } + } + + private fun selectCurrentParticipants() { + val list = arrayListOf
() + + for (participant in conference.participantList) { + list.add(participant.address) + } + + selectedAddresses.value = list + } +} diff --git a/app/src/main/java/org/linphone/activities/voip/viewmodels/ConferenceViewModel.kt b/app/src/main/java/org/linphone/activities/voip/viewmodels/ConferenceViewModel.kt new file mode 100644 index 000000000..38fef36ba --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/viewmodels/ConferenceViewModel.kt @@ -0,0 +1,395 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.voip.viewmodels + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.R +import org.linphone.activities.voip.data.ConferenceParticipantData +import org.linphone.activities.voip.data.ConferenceParticipantDeviceData +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 ConferenceViewModel : ViewModel() { + val conferenceExists = MutableLiveData() + val subject = MutableLiveData() + val isConferenceLocallyPaused = MutableLiveData() + val isVideoConference = MutableLiveData() + val isMeAdmin = MutableLiveData() + + val conference = MutableLiveData() + val conferenceCreationPending = MutableLiveData() + val conferenceParticipants = MutableLiveData>() + val conferenceParticipantDevices = MutableLiveData>() + val conferenceMosaicDisplayMode = MutableLiveData() + val conferenceActiveSpeakerDisplayMode = MutableLiveData() + + val isRecording = MutableLiveData() + val isRemotelyRecorded = MutableLiveData() + + val participantAdminStatusChangedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val maxParticipantsForMosaicLayout = corePreferences.maxConferenceParticipantsForMosaicLayout + + val speakingParticipant = MutableLiveData() + + private val conferenceListener = object : ConferenceListenerStub() { + override fun onParticipantAdded(conference: Conference, participant: Participant) { + Log.i("[Conference] Participant added: ${participant.address.asStringUriOnly()}") + updateParticipantsList(conference) + + val count = conferenceParticipants.value.orEmpty().size + if (count > maxParticipantsForMosaicLayout) { + Log.w("[Conference] More than $maxParticipantsForMosaicLayout participants ($count), forcing active speaker layout") + conferenceMosaicDisplayMode.value = false + conferenceActiveSpeakerDisplayMode.value = true + } + } + + override fun onParticipantRemoved(conference: Conference, participant: Participant) { + Log.i("[Conference] Participant removed: ${participant.address.asStringUriOnly()}") + updateParticipantsList(conference) + } + + override fun onParticipantDeviceAdded( + conference: Conference, + participantDevice: ParticipantDevice + ) { + Log.i("[Conference] Participant device added: ${participantDevice.address.asStringUriOnly()}") + addParticipantDevice(participantDevice) + } + + override fun onParticipantDeviceRemoved( + conference: Conference, + participantDevice: ParticipantDevice + ) { + Log.i("[Conference] Participant device removed: ${participantDevice.address.asStringUriOnly()}") + removeParticipantDevice(participantDevice) + } + + override fun onParticipantAdminStatusChanged( + conference: Conference, + participant: Participant + ) { + Log.i("[Conference] Participant admin status changed") + isMeAdmin.value = conference.me.isAdmin + updateParticipantsList(conference) + val participantData = conferenceParticipants.value.orEmpty().find { data -> data.participant.address.weakEqual(participant.address) } + if (participantData != null) { + participantAdminStatusChangedEvent.value = Event(participantData) + } else { + Log.w("[Conference] Failed to find participant [${participant.address.asStringUriOnly()}] in conferenceParticipants list") + } + } + + override fun onSubjectChanged(conference: Conference, subject: String) { + Log.i("[Conference] Subject changed: $subject") + this@ConferenceViewModel.subject.value = subject + } + + override fun onParticipantDeviceJoined(conference: Conference, device: ParticipantDevice) { + if (conference.isMe(device.address)) { + Log.i("[Conference] Entered conference") + isConferenceLocallyPaused.value = false + } + } + + override fun onParticipantDeviceLeft(conference: Conference, device: ParticipantDevice) { + if (conference.isMe(device.address)) { + Log.i("[Conference] Left conference") + isConferenceLocallyPaused.value = true + } + } + + override fun onParticipantDeviceIsSpeakingChanged( + conference: Conference, + participantDevice: ParticipantDevice, + isSpeaking: Boolean + ) { + Log.i("[Conference] Participant [${participantDevice.address.asStringUriOnly()}] is ${if (isSpeaking) "speaking" else "not speaking"}") + if (isSpeaking) { + val device = conferenceParticipantDevices.value.orEmpty().find { + it.participantDevice.address.weakEqual(participantDevice.address) + } + if (device != null && device != speakingParticipant.value) { + Log.i("[Conference] Found participant device") + speakingParticipant.value = device!! + } else if (device == null) { + Log.w("[Conference] Participant device [${participantDevice.address.asStringUriOnly()}] is speaking but couldn't find it in devices list") + } + } + } + + override fun onStateChanged(conference: Conference, state: Conference.State) { + Log.i("[Conference] State changed: $state") + isVideoConference.value = conference.currentParams.isVideoEnabled + + when (state) { + Conference.State.Created -> { + configureConference(conference) + conferenceCreationPending.value = false + } + Conference.State.TerminationPending -> { + terminateConference(conference) + } + else -> {} + } + } + } + + private val listener = object : CoreListenerStub() { + override fun onConferenceStateChanged( + core: Core, + conference: Conference, + state: Conference.State + ) { + Log.i("[Conference] Conference state changed: $state") + if (state == Conference.State.Instantiated) { + conferenceCreationPending.value = true + initConference(conference) + } + } + } + + init { + coreContext.core.addListener(listener) + + conferenceParticipants.value = arrayListOf() + conferenceParticipantDevices.value = arrayListOf() + conferenceMosaicDisplayMode.value = false + conferenceActiveSpeakerDisplayMode.value = false + + subject.value = AppUtils.getString(R.string.conference_default_title) + + var conference = coreContext.core.conference ?: coreContext.core.currentCall?.conference + if (conference == null) { + for (call in coreContext.core.calls) { + if (call.conference != null) { + conference = call.conference + break + } + } + } + if (conference != null) { + val state = conference.state + Log.i("[Conference] Found an existing conference: $conference in state $state") + if (state != Conference.State.TerminationPending && state != Conference.State.Terminated) { + initConference(conference) + if (state == Conference.State.Created) { + configureConference(conference) + } else { + conferenceCreationPending.value = true + } + } + } + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + conference.value?.removeListener(conferenceListener) + + conferenceParticipants.value.orEmpty().forEach(ConferenceParticipantData::destroy) + conferenceParticipantDevices.value.orEmpty().forEach(ConferenceParticipantDeviceData::destroy) + + super.onCleared() + } + + fun pauseConference() { + Log.i("[Conference] Leaving conference temporarily") + conference.value?.leave() + } + + fun resumeConference() { + Log.i("[Conference] Entering conference again") + conference.value?.enter() + } + + fun toggleRecording() { + if (conference.value?.isRecording == true) { + Log.i("[Conference] Stopping conference recording") + conference.value?.stopRecording() + } else { + val path = LinphoneUtils.getRecordingFilePathForConference() + Log.i("[Conference] Starting recording in file $path") + conference.value?.startRecording(path) + } + isRecording.value = conference.value?.isRecording + } + + fun initConference(conference: Conference) { + conferenceExists.value = true + + this@ConferenceViewModel.conference.value = conference + conference.addListener(conferenceListener) + + isRecording.value = conference.isRecording + + updateConferenceLayout(conference) + } + + fun configureConference(conference: Conference) { + updateParticipantsList(conference) + updateParticipantsDevicesList(conference) + + isConferenceLocallyPaused.value = !conference.isIn + isMeAdmin.value = conference.me.isAdmin + isVideoConference.value = conference.currentParams.isVideoEnabled + subject.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 + } + + updateConferenceLayout(conference) + } + + fun addCallsToConference() { + Log.i("[Conference] Trying to merge all calls into existing conference") + val conf = conference.value + conf ?: return + + for (call in coreContext.core.calls) { + if (call.conference == null) { + Log.i("[Conference] Adding call [$call] as participant for conference [$conf]") + conf.addParticipant(call) + } + } + if (!conf.isIn) { + Log.i("[Conference] Conference was paused, resuming it") + conf.enter() + } + } + + private fun updateConferenceLayout(conference: Conference) { + val layout = conference.layout + conferenceMosaicDisplayMode.value = layout == ConferenceLayout.Grid || layout == ConferenceLayout.Legacy + conferenceActiveSpeakerDisplayMode.value = layout == ConferenceLayout.ActiveSpeaker + Log.i("[Conference] Conference current layout is: $layout") + } + + private fun terminateConference(conference: Conference) { + conferenceExists.value = false + isVideoConference.value = false + + conference.removeListener(conferenceListener) + + conferenceParticipants.value.orEmpty().forEach(ConferenceParticipantData::destroy) + conferenceParticipantDevices.value.orEmpty().forEach(ConferenceParticipantDeviceData::destroy) + conferenceParticipants.value = arrayListOf() + conferenceParticipantDevices.value = arrayListOf() + } + + private fun updateParticipantsList(conference: Conference) { + conferenceParticipants.value.orEmpty().forEach(ConferenceParticipantData::destroy) + val participants = arrayListOf() + + val participantsList = conference.participantList + Log.i("[Conference] Conference has ${participantsList.size} participants") + for (participant in participantsList) { + val participantDevices = participant.devices + Log.i("[Conference] Participant found: ${participant.address.asStringUriOnly()} with ${participantDevices.size} device(s)") + + val participantData = ConferenceParticipantData(conference, participant) + participants.add(participantData) + } + + conferenceParticipants.value = participants + } + + private fun updateParticipantsDevicesList(conference: Conference) { + conferenceParticipantDevices.value.orEmpty().forEach(ConferenceParticipantDeviceData::destroy) + val devices = arrayListOf() + + val participantsList = conference.participantList + Log.i("[Conference] Conference has ${participantsList.size} participants") + for (participant in participantsList) { + val participantDevices = participant.devices + Log.i("[Conference] Participant found: ${participant.address.asStringUriOnly()} with ${participantDevices.size} device(s)") + + for (device in participantDevices) { + Log.i("[Conference] Participant device found: ${device.name} (${device.address.asStringUriOnly()})") + val deviceData = ConferenceParticipantDeviceData(device, false) + devices.add(deviceData) + } + } + if (devices.isNotEmpty()) { + speakingParticipant.value = devices.first() + } + + for (device in conference.me.devices) { + Log.i("[Conference] Participant device for myself found: ${device.name} (${device.address.asStringUriOnly()})") + val deviceData = ConferenceParticipantDeviceData(device, true) + devices.add(deviceData) + } + + conferenceParticipantDevices.value = devices + } + + private fun addParticipantDevice(device: ParticipantDevice) { + val devices = arrayListOf() + devices.addAll(conferenceParticipantDevices.value.orEmpty()) + + val existingDevice = devices.find { + it.participantDevice.address.weakEqual(device.address) + } + if (existingDevice != null) { + Log.e("[Conference] Participant is already in devices list: ${device.name} (${device.address.asStringUriOnly()})") + return + } + + Log.i("[Conference] New participant device found: ${device.name} (${device.address.asStringUriOnly()})") + val deviceData = ConferenceParticipantDeviceData(device, false) + devices.add(deviceData) + + if (speakingParticipant.value == null) { + speakingParticipant.value = deviceData + } + + conferenceParticipantDevices.value = devices + } + + private fun removeParticipantDevice(device: ParticipantDevice) { + val devices = arrayListOf() + + for (participantDevice in conferenceParticipantDevices.value.orEmpty()) { + if (participantDevice.participantDevice.address.asStringUriOnly() != device.address.asStringUriOnly()) { + devices.add(participantDevice) + } + } + if (devices.size == conferenceParticipantDevices.value.orEmpty().size) { + Log.e("[Conference] Failed to remove participant device: ${device.name} (${device.address.asStringUriOnly()})") + } else { + Log.i("[Conference] Participant device removed: ${device.name} (${device.address.asStringUriOnly()})") + } + + conferenceParticipantDevices.value = devices + } +} diff --git a/app/src/main/java/org/linphone/activities/voip/viewmodels/ControlsViewModel.kt b/app/src/main/java/org/linphone/activities/voip/viewmodels/ControlsViewModel.kt new file mode 100644 index 000000000..65759f5fa --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/viewmodels/ControlsViewModel.kt @@ -0,0 +1,456 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.voip.viewmodels + +import android.Manifest +import android.animation.ValueAnimator +import android.view.MotionEvent +import android.view.View +import android.view.animation.LinearInterpolator +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.R +import org.linphone.core.* +import org.linphone.core.tools.Log +import org.linphone.utils.AppUtils +import org.linphone.utils.AudioRouteUtils +import org.linphone.utils.Event +import org.linphone.utils.PermissionHelper + +class ControlsViewModel : ViewModel() { + val isMicrophoneMuted = MutableLiveData() + + val isMuteMicrophoneEnabled = MutableLiveData() + + val isSpeakerSelected = MutableLiveData() + + val isBluetoothHeadsetSelected = MutableLiveData() + + val audioRoutesSelected = MutableLiveData() + + val audioRoutesEnabled = MutableLiveData() + + val isVideoAvailable = MutableLiveData() + + val isVideoEnabled = MutableLiveData() + + val isVideoUpdateInProgress = MutableLiveData() + + val isSwitchCameraAvailable = MutableLiveData() + + val isOutgoingEarlyMedia = MutableLiveData() + + val showExtras = MutableLiveData() + + val fullScreenMode = MutableLiveData() + + val pipMode = MutableLiveData() + + val chatRoomCreationInProgress = MutableLiveData() + + val numpadVisible = MutableLiveData() + + val dtmfHistory = MutableLiveData() + + val callStatsVisible = MutableLiveData() + + val proximitySensorEnabled = MediatorLiveData() + + val goToConferenceParticipantsListEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val goToChatEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val goToCallsListEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val goToConferenceLayoutSettings: MutableLiveData> by lazy { + MutableLiveData>() + } + + val askPermissionEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val goToDialer: MutableLiveData> by lazy { + MutableLiveData>() + } + + private val nonEarpieceOutputAudioDevice = MutableLiveData() + + private var previewX: Float = 0f + private var previewY: Float = 0f + val previewTouchListener = View.OnTouchListener { view, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + previewX = view.x - event.rawX + previewY = view.y - event.rawY + true + } + MotionEvent.ACTION_MOVE -> { + view.animate() + .x(event.rawX + previewX) + .y(event.rawY + previewY) + .setDuration(0) + .start() + true + } + else -> { + view.performClick() + false + } + } + } + + private val listener: CoreListenerStub = object : CoreListenerStub() { + override fun onCallStateChanged( + core: Core, + call: Call, + state: Call.State, + message: String + ) { + Log.i("[Call Controls] State changed: $state") + + isOutgoingEarlyMedia.value = state == Call.State.OutgoingEarlyMedia + if (state == Call.State.StreamsRunning) { + isVideoUpdateInProgress.value = false + } else if (state == Call.State.PausedByRemote) { + fullScreenMode.value = false + } + + if (core.currentCall?.currentParams?.isVideoEnabled == true && !PermissionHelper.get().hasCameraPermission()) { + askPermissionEvent.value = Event(Manifest.permission.CAMERA) + } + + updateUI() + } + + override fun onAudioDeviceChanged(core: Core, audioDevice: AudioDevice) { + Log.i("[Call Controls] Audio device changed: ${audioDevice.deviceName}") + + nonEarpieceOutputAudioDevice.value = audioDevice.type != AudioDevice.Type.Earpiece + updateSpeakerState() + updateBluetoothHeadsetState() + } + + override fun onAudioDevicesListUpdated(core: Core) { + Log.i("[Call Controls] 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() + } + } + } + } + + val extraButtonsMenuTranslateY = MutableLiveData() + private val extraButtonsMenuAnimator: ValueAnimator by lazy { + ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.voip_call_extra_buttons_translate_y), 0f).apply { + addUpdateListener { + val value = it.animatedValue as Float + extraButtonsMenuTranslateY.value = value + } + duration = if (corePreferences.enableAnimations) 500 else 0 + } + } + + val audioRoutesMenuTranslateY = MutableLiveData() + private val audioRoutesMenuAnimator: ValueAnimator by lazy { + ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.voip_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 bouncyCounterTranslateY = MutableLiveData() + + private val bounceAnimator: ValueAnimator by lazy { + ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.voip_counter_bounce_offset), 0f).apply { + addUpdateListener { + val value = it.animatedValue as Float + bouncyCounterTranslateY.value = value + } + interpolator = LinearInterpolator() + duration = 250 + repeatMode = ValueAnimator.REVERSE + repeatCount = ValueAnimator.INFINITE + } + } + + init { + coreContext.core.addListener(listener) + + fullScreenMode.value = false + extraButtonsMenuTranslateY.value = AppUtils.getDimension(R.dimen.voip_call_extra_buttons_translate_y) + audioRoutesMenuTranslateY.value = AppUtils.getDimension(R.dimen.voip_audio_routes_menu_translate_y) + audioRoutesSelected.value = false + + nonEarpieceOutputAudioDevice.value = coreContext.core.outputAudioDevice?.type != AudioDevice.Type.Earpiece + proximitySensorEnabled.value = shouldProximitySensorBeEnabled() + proximitySensorEnabled.addSource(isVideoEnabled) { + proximitySensorEnabled.value = shouldProximitySensorBeEnabled() + } + proximitySensorEnabled.addSource(nonEarpieceOutputAudioDevice) { + proximitySensorEnabled.value = shouldProximitySensorBeEnabled() + } + + updateUI() + + if (corePreferences.enableAnimations) bounceAnimator.start() + } + + override fun onCleared() { + coreContext.core.removeListener(listener) + + super.onCleared() + } + + fun hangUp() { + val core = coreContext.core + when { + core.currentCall != null -> core.currentCall?.terminate() + core.conference?.isIn == true -> core.terminateConference() + else -> core.terminateAllCalls() + } + } + + fun answer() { + val currentCall = coreContext.core.currentCall + if (currentCall != null) { + coreContext.answerCall(currentCall) + } else { + Log.e("[Controls] Cant't find any current call to answer") + } + } + + fun toggleMuteMicrophone() { + if (!PermissionHelper.get().hasRecordAudioPermission()) { + askPermissionEvent.value = Event(Manifest.permission.RECORD_AUDIO) + return + } + + val micEnabled = coreContext.core.isMicEnabled + coreContext.core.isMicEnabled = !micEnabled + updateMicState() + } + + fun toggleSpeaker() { + if (AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed()) { + forceEarpieceAudioRoute() + } else { + forceSpeakerAudioRoute() + } + } + + fun toggleRoutesMenu() { + audioRoutesSelected.value = audioRoutesSelected.value != true + if (audioRoutesSelected.value == true) { + audioRoutesMenuAnimator.start() + } else { + audioRoutesMenuAnimator.reverse() + } + } + + fun forceEarpieceAudioRoute() { + if (AudioRouteUtils.isHeadsetAudioRouteAvailable()) { + Log.i("[Call Controls] Headset found, route audio to it instead of earpiece") + AudioRouteUtils.routeAudioToHeadset() + } else { + AudioRouteUtils.routeAudioToEarpiece() + } + } + + fun forceSpeakerAudioRoute() { + AudioRouteUtils.routeAudioToSpeaker() + } + + fun forceBluetoothAudioRoute() { + AudioRouteUtils.routeAudioToBluetooth() + } + + fun toggleVideo() { + if (!PermissionHelper.get().hasCameraPermission()) { + askPermissionEvent.value = Event(Manifest.permission.CAMERA) + return + } + + val core = coreContext.core + val currentCall = core.currentCall + 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 switchCamera() { + coreContext.switchCamera() + } + + fun showExtraButtons() { + extraButtonsMenuAnimator.start() + showExtras.value = true + } + + fun hideExtraButtons(skipAnimation: Boolean) { + // Animation must be skipped when called from Fragment's onPause() ! + if (skipAnimation) { + extraButtonsMenuTranslateY.value = AppUtils.getDimension(R.dimen.voip_call_extra_buttons_translate_y) + } else { + extraButtonsMenuAnimator.reverse() + } + showExtras.value = false + chatRoomCreationInProgress.value = false + } + + fun toggleFullScreen() { + if (fullScreenMode.value == false && isVideoEnabled.value == false) return + fullScreenMode.value = fullScreenMode.value != true + } + + fun goToConferenceParticipantsList() { + goToConferenceParticipantsListEvent.value = Event(true) + } + + fun goToChat() { + chatRoomCreationInProgress.value = true + goToChatEvent.value = Event(true) + } + + fun showNumpad() { + hideExtraButtons(false) + numpadVisible.value = true + } + + fun hideNumpad() { + numpadVisible.value = false + } + + fun handleDtmfClick(key: Char) { + dtmfHistory.value = "${dtmfHistory.value.orEmpty()}$key" + coreContext.core.playDtmf(key, 1) + coreContext.core.currentCall?.sendDtmf(key) + } + + fun goToCallsList() { + goToCallsListEvent.value = Event(true) + } + + fun showCallStats() { + hideExtraButtons(false) + callStatsVisible.value = true + } + + fun hideCallStats() { + callStatsVisible.value = false + } + + fun goToConferenceLayout() { + goToConferenceLayoutSettings.value = Event(true) + } + + fun goToDialerForCallTransfer() { + goToDialer.value = Event(true) + } + + fun goToDialerForNewCall() { + goToDialer.value = Event(false) + } + + private fun updateUI() { + updateVideoAvailable() + updateVideoEnabled() + updateMicState() + updateSpeakerState() + updateAudioRoutesState() + } + + fun updateMicState() { + isMicrophoneMuted.value = !PermissionHelper.get().hasRecordAudioPermission() || !coreContext.core.isMicEnabled + isMuteMicrophoneEnabled.value = coreContext.core.currentCall != null || coreContext.core.conference?.isIn == true + } + + private fun updateSpeakerState() { + isSpeakerSelected.value = AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed() + } + + private fun updateBluetoothHeadsetState() { + isBluetoothHeadsetSelected.value = AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed() + } + + private fun updateAudioRoutesState() { + val bluetoothDeviceAvailable = AudioRouteUtils.isBluetoothAudioRouteAvailable() + audioRoutesEnabled.value = bluetoothDeviceAvailable + + if (!bluetoothDeviceAvailable) { + audioRoutesSelected.value = false + } + } + + 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.core.currentCall?.currentParams?.isVideoEnabled ?: false + if (enabled && isVideoEnabled.value == false) { + Log.i("[Call Controls] Video is being turned on") + if (corePreferences.routeAudioToSpeakerWhenVideoIsEnabled) { + // Do not turn speaker on when video is enabled if headset or bluetooth is used + if (!AudioRouteUtils.isHeadsetAudioRouteAvailable() && + !AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed() + ) { + Log.i("[Call Controls] Video enabled and no wired headset not bluetooth in use, routing audio to speaker") + AudioRouteUtils.routeAudioToSpeaker() + } + } + } + isVideoEnabled.value = enabled + isSwitchCameraAvailable.value = enabled && coreContext.showSwitchCameraButton() + } + + private fun shouldProximitySensorBeEnabled(): Boolean { + return !(isVideoEnabled.value ?: false) && !(nonEarpieceOutputAudioDevice.value ?: false) + } +} diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/StatisticsListViewModel.kt b/app/src/main/java/org/linphone/activities/voip/viewmodels/StatisticsListViewModel.kt similarity index 92% rename from app/src/main/java/org/linphone/activities/call/viewmodels/StatisticsListViewModel.kt rename to app/src/main/java/org/linphone/activities/voip/viewmodels/StatisticsListViewModel.kt index 47bf460e7..e4c6f8915 100644 --- a/app/src/main/java/org/linphone/activities/call/viewmodels/StatisticsListViewModel.kt +++ b/app/src/main/java/org/linphone/activities/voip/viewmodels/StatisticsListViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2020 Belledonne Communications SARL. + * Copyright (c) 2010-2021 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,12 +17,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.activities.call.viewmodels +package org.linphone.activities.voip.viewmodels import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.activities.call.data.CallStatisticsData +import org.linphone.activities.voip.data.CallStatisticsData import org.linphone.core.Call import org.linphone.core.Core import org.linphone.core.CoreListenerStub diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/StatusViewModel.kt b/app/src/main/java/org/linphone/activities/voip/viewmodels/StatusViewModel.kt similarity index 95% rename from app/src/main/java/org/linphone/activities/call/viewmodels/StatusViewModel.kt rename to app/src/main/java/org/linphone/activities/voip/viewmodels/StatusViewModel.kt index 425872eeb..11d3fac31 100644 --- a/app/src/main/java/org/linphone/activities/call/viewmodels/StatusViewModel.kt +++ b/app/src/main/java/org/linphone/activities/voip/viewmodels/StatusViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2020 Belledonne Communications SARL. + * Copyright (c) 2010-2021 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -17,7 +17,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.activities.call.viewmodels +package org.linphone.activities.voip.viewmodels import androidx.lifecycle.MutableLiveData import org.linphone.LinphoneApplication.Companion.coreContext @@ -38,6 +38,10 @@ class StatusViewModel : StatusViewModel() { MutableLiveData>() } + val showCallStatsEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + private val listener = object : CoreListenerStub() { override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) { updateCallQualityIcon() @@ -96,6 +100,10 @@ class StatusViewModel : StatusViewModel() { } } + fun showCallStats() { + showCallStatsEvent.value = Event(true) + } + fun updateEncryptionInfo(call: Call) { if (call.dir == Call.Dir.Incoming && call.state == Call.State.IncomingReceived && call.core.isMediaEncryptionMandatory) { // If the incoming call view is displayed while encryption is mandatory, diff --git a/app/src/main/java/org/linphone/activities/voip/views/HorizontalScrollDotsView.kt b/app/src/main/java/org/linphone/activities/voip/views/HorizontalScrollDotsView.kt new file mode 100644 index 000000000..a7b892d1f --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/views/HorizontalScrollDotsView.kt @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.voip.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.util.AttributeSet +import android.view.View +import android.widget.HorizontalScrollView +import java.lang.Exception +import kotlin.math.ceil +import kotlin.math.roundToInt +import org.linphone.R +import org.linphone.core.tools.Log +import org.linphone.utils.AppUtils + +class HorizontalScrollDotsView : View { + private var count = 2 + private var selected = 0 + + private var radius: Float = 5f + private var margin: Float = 2f + private var screenWidth: Float = 0f + private var itemWidth: Float = 0f + + private lateinit var dotPaint: Paint + private lateinit var selectedDotPaint: Paint + + private var horizontalScrollViewRef = 0 + private lateinit var horizontalScrollView: HorizontalScrollView + private val scrollListener = OnScrollChangeListener { v, scrollX, _, _, _ -> + val childWidth: Int = (v as HorizontalScrollView).getChildAt(0).measuredWidth + val scrollViewWidth = v.measuredWidth + val scrollableX = childWidth - scrollViewWidth + + if (scrollableX > 0) { + val percent = (scrollX.toFloat() * 100 / scrollableX).toDouble() + if (count > 1) { + val selectedDot = percent / (100 / (count - 1)) + val dot = selectedDot.roundToInt() + if (dot != selected) { + setSelectedDot(dot) + } + } + } + } + + constructor(context: Context) : super(context) { init(context) } + + constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0) + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + init(context) + + context.theme.obtainStyledAttributes( + attrs, + R.styleable.HorizontalScrollDot, + defStyleAttr, 0 + ).apply { + try { + radius = getDimension(R.styleable.HorizontalScrollDot_dotRadius, 5f) + + count = getInt(R.styleable.HorizontalScrollDot_dotCount, 1) + + val color = getColor(R.styleable.HorizontalScrollDot_dotColor, context.resources.getColor(R.color.voip_gray_background)) + dotPaint.color = color + val selectedColor = getColor(R.styleable.HorizontalScrollDot_selectedDotColor, context.resources.getColor(R.color.voip_dark_gray)) + selectedDotPaint.color = selectedColor + + selected = getInt(R.styleable.HorizontalScrollDot_selectedDot, 1) + + horizontalScrollViewRef = getResourceId(R.styleable.HorizontalScrollDot_horizontalScrollView, 0) + Log.d("[Horizontal Scroll Dots] HorizontalScrollView reference set is $horizontalScrollViewRef") + + invalidate() + } catch (e: Exception) { + Log.e("[Horizontal Scroll Dots] $e") + } finally { + recycle() + } + } + } + + fun init(context: Context) { + radius = AppUtils.dpToPixels(context, 5f) + margin = AppUtils.dpToPixels(context, 5f) + + dotPaint = Paint() + dotPaint.color = Color.parseColor("#D8D8D8") + selectedDotPaint = Paint() + selectedDotPaint.color = Color.parseColor("#4B5964") + + val screenRect = Rect() + getWindowVisibleDisplayFrame(screenRect) + screenWidth = screenRect.width().toFloat() + val marginBetweenItems = context.resources.getDimension(R.dimen.voip_active_speaker_miniature_margin) + itemWidth = context.resources.getDimension(R.dimen.voip_active_speaker_miniature_size) + marginBetweenItems + Log.d("[Horizontal Scroll Dots] Screen width is $screenWidth and item width is $itemWidth") + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + + if (horizontalScrollViewRef > 0) { + try { + horizontalScrollView = (parent as View).findViewById(horizontalScrollViewRef) + horizontalScrollView.setOnScrollChangeListener(scrollListener) + Log.d("[Horizontal Scroll Dots] HorizontalScrollView scroll listener set") + } catch (e: Exception) { + Log.e("[Horizontal Scroll Dots] Failed to find HorizontalScrollView from id $horizontalScrollViewRef: $e") + } + } else { + Log.e("[Horizontal Scroll Dots] No HorizontalScrollView reference given") + } + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + for (i in 0 until count) { + if (i == selected) { + canvas.drawCircle( + (i + 1) * margin + (i * 2 + 1) * radius, + radius, + radius, + selectedDotPaint + ) + } else { + canvas.drawCircle( + (i + 1) * margin + (i * 2 + 1) * radius, + radius, + radius, + dotPaint + ) + } + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + val width = ((radius * 2 + margin) * count + margin).toInt() + val height: Int = (radius * 2).toInt() + + setMeasuredDimension(width, height) + } + + fun setDotCount(count: Int) { + this.count = count + requestLayout() + invalidate() + } + + fun setItemCount(items: Int) { + val itemsPerScreen = (screenWidth / itemWidth) + val dots = ceil(items.toDouble() / itemsPerScreen).toInt() + + Log.d("[Horizontal Scroll Dots] Calculated $count for $items items ($itemsPerScreen items fit in screen width), given that screen width is $screenWidth and item width is $itemWidth") + setDotCount(dots) + } + + fun setSelectedDot(index: Int) { + selected = index + invalidate() + } +} diff --git a/app/src/main/java/org/linphone/activities/voip/views/RoundCornersTextureView.kt b/app/src/main/java/org/linphone/activities/voip/views/RoundCornersTextureView.kt new file mode 100644 index 000000000..68ae85ca0 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/voip/views/RoundCornersTextureView.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.activities.voip.views + +import android.content.Context +import android.graphics.Outline +import android.graphics.Rect +import android.util.AttributeSet +import android.view.View +import android.view.ViewOutlineProvider +import org.linphone.R +import org.linphone.mediastream.video.capture.CaptureTextureView + +class RoundCornersTextureView : CaptureTextureView { + constructor(context: Context) : super(context) { + mAlignTopRight = true + mDisplayMode = DisplayMode.BLACK_BARS + setRoundCorners() + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + readAttributes(attrs) + setRoundCorners() + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + readAttributes(attrs) + setRoundCorners() + } + + private fun readAttributes(attrs: AttributeSet) { + context.theme.obtainStyledAttributes( + attrs, + R.styleable.RoundCornersTextureView, + 0, + 0 + ).apply { + try { + mAlignTopRight = getBoolean(R.styleable.RoundCornersTextureView_alignTopRight, true) + val mode = getInteger(R.styleable.RoundCornersTextureView_displayMode, DisplayMode.BLACK_BARS.ordinal) + mDisplayMode = when (mode) { + 1 -> DisplayMode.OCCUPY_ALL_SPACE + 2 -> DisplayMode.HYBRID + else -> DisplayMode.BLACK_BARS + } + } finally { + recycle() + } + } + } + + private fun setRoundCorners() { + outlineProvider = object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + val rect = if (previewRectF != null && + actualDisplayMode == DisplayMode.BLACK_BARS && + mAlignTopRight + ) { + Rect( + previewRectF.left.toInt(), + previewRectF.top.toInt(), + previewRectF.right.toInt(), + previewRectF.bottom.toInt() + ) + } else { + Rect( + 0, + 0, + width, + height + ) + } + outline.setRoundRect(rect, context.resources.getDimension(R.dimen.voip_round_corners_texture_view_radius)) + } + } + clipToOutline = true + } + + override fun setAspectRatio(width: Int, height: Int) { + super.setAspectRatio(width, height) + + val previewSize = previewVideoSize + if (previewSize.width > 0 && previewSize.height > 0) { + setRoundCorners() + } + } +} diff --git a/app/src/main/java/org/linphone/compatibility/Api21Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api21Compatibility.kt index db1a1b764..ecea7b14c 100644 --- a/app/src/main/java/org/linphone/compatibility/Api21Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api21Compatibility.kt @@ -34,6 +34,8 @@ import android.os.Environment import android.os.Vibrator import android.provider.MediaStore import android.provider.Settings +import android.view.View +import android.view.Window import android.view.WindowManager import android.view.inputmethod.EditorInfo import org.linphone.R @@ -245,5 +247,21 @@ class Api21Compatibility { fun startForegroundService(context: Context, intent: Intent) { context.startService(intent) } + + fun hideAndroidSystemUI(hide: Boolean, window: Window) { + val decorView = window.decorView + val uiOptions = if (hide) { + View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + } else { + View.SYSTEM_UI_FLAG_VISIBLE + } + decorView.systemUiVisibility = uiOptions + + if (hide) { + window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) + } + } } } diff --git a/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt index a6e7be818..37f40cf90 100644 --- a/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt @@ -53,11 +53,11 @@ class Api26Compatibility { fun enterPipMode(activity: Activity) { val supportsPip = activity.packageManager .hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) - Log.i("[Call] Is picture in picture supported: $supportsPip") + Log.i("[Call] Is PiP supported: $supportsPip") if (supportsPip) { val params = PictureInPictureParams.Builder().build() if (!activity.enterPictureInPictureMode(params)) { - Log.e("[Call] Failed to enter picture in picture mode") + Log.e("[Call] Failed to enter PiP mode") } } } diff --git a/app/src/main/java/org/linphone/compatibility/Api30Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api30Compatibility.kt index 70637bbd2..556d755e0 100644 --- a/app/src/main/java/org/linphone/compatibility/Api30Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api30Compatibility.kt @@ -24,6 +24,9 @@ import android.annotation.TargetApi import android.app.Activity import android.content.Context import android.content.pm.ShortcutManager +import android.view.Window +import android.view.WindowInsets +import android.view.WindowInsetsController import androidx.fragment.app.Fragment import org.linphone.core.ChatRoom import org.linphone.core.tools.Log @@ -69,5 +72,19 @@ class Api30Compatibility { val shortcutsToRemoveList = arrayListOf(id) shortcutManager.removeLongLivedShortcuts(shortcutsToRemoveList) } + + fun hideAndroidSystemUI(hide: Boolean, window: Window) { + if (hide) { + window.setDecorFitsSystemWindows(false) + window.insetsController?.let { + it.systemBarsBehavior = + WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + it.hide(WindowInsets.Type.systemBars()) + } + } else { + window.setDecorFitsSystemWindows(true) + window.insetsController?.show(WindowInsets.Type.systemBars()) + } + } } } diff --git a/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt index 82262693e..44426abe8 100644 --- a/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt @@ -20,16 +20,20 @@ package org.linphone.compatibility import android.annotation.TargetApi +import android.app.Activity import android.app.Notification import android.app.PendingIntent import android.app.Person +import android.app.PictureInPictureParams import android.content.Context +import android.content.pm.PackageManager import androidx.core.content.ContextCompat import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.contact.Contact import org.linphone.core.Call +import org.linphone.core.tools.Log import org.linphone.notifications.Notifiable import org.linphone.notifications.NotificationsManager import org.linphone.utils.ImageUtils @@ -91,7 +95,8 @@ class Api31Compatibility { channel: String, notificationsManager: NotificationsManager ): Notification { - val contact: Contact? = coreContext.contactsManager.findContactByAddress(call.remoteAddress) + val contact: Contact? = + coreContext.contactsManager.findContactByAddress(call.remoteAddress) val pictureUri = contact?.getContactThumbnailPictureUri() val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri) val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress) @@ -122,7 +127,9 @@ class Api31Compatibility { val builder = Notification.Builder( context, channel ) - .setStyle(Notification.CallStyle.forOngoingCall(caller, declineIntent).setIsVideo(isVideo)) + .setStyle( + Notification.CallStyle.forOngoingCall(caller, declineIntent).setIsVideo(isVideo) + ) .setSmallIcon(iconResourceId) .setAutoCancel(false) .setCategory(Notification.CATEGORY_CALL) @@ -131,7 +138,10 @@ class Api31Compatibility { .setShowWhen(true) .setOngoing(true) .setColor(ContextCompat.getColor(context, R.color.notification_led_color)) - .setFullScreenIntent(pendingIntent, true) // This is required for CallStyle notification + .setFullScreenIntent( + pendingIntent, + true + ) // This is required for CallStyle notification if (!corePreferences.preventInterfaceFromShowingUp) { builder.setContentIntent(pendingIntent) @@ -139,5 +149,16 @@ class Api31Compatibility { return builder.build() } + + fun enableAutoEnterPiP(activity: Activity, enable: Boolean) { + val supportsPip = activity.packageManager + .hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) + Log.i("[Call] Is PiP supported: $supportsPip") + if (supportsPip) { + val params = PictureInPictureParams.Builder().setAutoEnterEnabled(enable).build() + activity.setPictureInPictureParams(params) + Log.i("[Call] PiP auto enter enabled params set to $enable") + } + } } } diff --git a/app/src/main/java/org/linphone/compatibility/Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Compatibility.kt index e01d09c95..316b02ea7 100644 --- a/app/src/main/java/org/linphone/compatibility/Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Compatibility.kt @@ -31,6 +31,7 @@ import android.os.Build import android.os.Vibrator import android.telephony.TelephonyManager import android.view.View +import android.view.Window import android.view.WindowManager import androidx.core.app.NotificationManagerCompat import androidx.fragment.app.Fragment @@ -219,11 +220,17 @@ class Compatibility { } fun enterPipMode(activity: Activity) { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { + if (Version.sdkStrictlyBelow(Version.API31_ANDROID_12) && Version.sdkAboveOrEqual(Version.API26_O_80)) { Api26Compatibility.enterPipMode(activity) } } + fun enableAutoEnterPiP(activity: Activity, enable: Boolean) { + if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) { + Api31Compatibility.enableAutoEnterPiP(activity, enable) + } + } + fun eventVibration(vibrator: Vibrator) { if (Version.sdkAboveOrEqual(Version.API26_O_80)) { Api26Compatibility.eventVibration(vibrator) @@ -239,6 +246,14 @@ class Compatibility { return false } + fun hideAndroidSystemUI(hide: Boolean, window: Window) { + if (Version.sdkAboveOrEqual(Version.API30_ANDROID_11)) { + Api30Compatibility.hideAndroidSystemUI(hide, window) + } else { + Api21Compatibility.hideAndroidSystemUI(hide, window) + } + } + /* Contacts */ fun createShortcutsToContacts(context: Context) { diff --git a/app/src/main/java/org/linphone/compatibility/XiaomiCompatibility.kt b/app/src/main/java/org/linphone/compatibility/XiaomiCompatibility.kt index 05b6a482c..0f5dbadbe 100644 --- a/app/src/main/java/org/linphone/compatibility/XiaomiCompatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/XiaomiCompatibility.kt @@ -54,7 +54,7 @@ class XiaomiCompatibility { val builder = NotificationCompat.Builder(context, context.getString(R.string.notification_channel_incoming_call_id)) .addPerson(notificationsManager.getPerson(contact, displayName, roundPicture)) .setSmallIcon(R.drawable.topbar_call_notification) - .setLargeIcon(roundPicture ?: BitmapFactory.decodeResource(context.resources, R.drawable.avatar)) + .setLargeIcon(roundPicture ?: BitmapFactory.decodeResource(context.resources, R.drawable.voip_single_contact_avatar)) .setContentTitle(displayName) .setContentText(address) .setSubText(context.getString(R.string.incoming_call_notification_title)) diff --git a/app/src/main/java/org/linphone/contact/BigContactAvatarView.kt b/app/src/main/java/org/linphone/contact/BigContactAvatarView.kt index a608d0c18..d72c97d43 100644 --- a/app/src/main/java/org/linphone/contact/BigContactAvatarView.kt +++ b/app/src/main/java/org/linphone/contact/BigContactAvatarView.kt @@ -25,7 +25,7 @@ import android.view.LayoutInflater import android.view.View import android.widget.LinearLayout import androidx.databinding.DataBindingUtil -import org.linphone.LinphoneApplication +import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.databinding.ContactAvatarBigBinding import org.linphone.utils.AppUtils @@ -68,6 +68,6 @@ class BigContactAvatarView : LinearLayout { binding.initials = initials binding.generatedAvatarVisibility = initials.isNotEmpty() && initials != "+" binding.imagePath = contact?.getContactPictureUri() - binding.borderVisibility = LinphoneApplication.corePreferences.showBorderOnBigContactAvatar + binding.borderVisibility = corePreferences.showBorderOnBigContactAvatar } } diff --git a/app/src/main/java/org/linphone/contact/Contact.kt b/app/src/main/java/org/linphone/contact/Contact.kt index 8cba22e3a..64ad43bf8 100644 --- a/app/src/main/java/org/linphone/contact/Contact.kt +++ b/app/src/main/java/org/linphone/contact/Contact.kt @@ -136,7 +136,7 @@ open class Contact : Comparable { } open fun getContactPictureUri(): Uri? { - return null + return thumbnailUri } open fun getPerson(): Person { @@ -150,7 +150,7 @@ open class Contact : Comparable { val icon = if (bm == null) IconCompat.createWithResource( coreContext.context, - R.drawable.avatar + R.drawable.voip_single_contact_avatar ) else IconCompat.createWithAdaptiveBitmap(bm) if (icon != null) { personBuilder.setIcon(icon) diff --git a/app/src/main/java/org/linphone/contact/ContactDataInterface.kt b/app/src/main/java/org/linphone/contact/ContactDataInterface.kt index 5e58786ed..17a9a9fd0 100644 --- a/app/src/main/java/org/linphone/contact/ContactDataInterface.kt +++ b/app/src/main/java/org/linphone/contact/ContactDataInterface.kt @@ -21,9 +21,10 @@ package org.linphone.contact import androidx.lifecycle.MutableLiveData import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.activities.main.viewmodels.ErrorReportingViewModel +import org.linphone.activities.main.viewmodels.MessageNotifierViewModel import org.linphone.core.Address import org.linphone.core.ChatRoomSecurityLevel +import org.linphone.utils.AppUtils import org.linphone.utils.LinphoneUtils interface ContactDataInterface { @@ -41,6 +42,8 @@ open class GenericContactData(private val sipAddress: Address) : ContactDataInte final override val contact: MutableLiveData = MutableLiveData() final override val displayName: MutableLiveData = MutableLiveData() final override val securityLevel: MutableLiveData = MutableLiveData() + val initials = MutableLiveData() + val displayInitials = MutableLiveData() private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() { override fun onContactUpdated(contact: Contact) { @@ -60,12 +63,20 @@ open class GenericContactData(private val sipAddress: Address) : ContactDataInte private fun contactLookup() { displayName.value = LinphoneUtils.getDisplayName(sipAddress) - contact.value = - coreContext.contactsManager.findContactByAddress(sipAddress) + + val c = coreContext.contactsManager.findContactByAddress(sipAddress) + contact.value = c + + initials.value = if (c != null) { + AppUtils.getInitials(c.fullName ?: c.firstName + " " + c.lastName) + } else { + AppUtils.getInitials(displayName.value ?: "") + } + displayInitials.value = initials.value.orEmpty().isNotEmpty() && initials.value.orEmpty() != "+" } } -abstract class GenericContactViewModel(private val sipAddress: Address) : ErrorReportingViewModel(), ContactDataInterface { +abstract class GenericContactViewModel(private val sipAddress: Address) : MessageNotifierViewModel(), ContactDataInterface { final override val contact: MutableLiveData = MutableLiveData() final override val displayName: MutableLiveData = MutableLiveData() final override val securityLevel: MutableLiveData = MutableLiveData() diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatRoomCreationContactData.kt b/app/src/main/java/org/linphone/contact/ContactSelectionData.kt similarity index 92% rename from app/src/main/java/org/linphone/activities/main/chat/data/ChatRoomCreationContactData.kt rename to app/src/main/java/org/linphone/contact/ContactSelectionData.kt index 1c02c2e87..192322ba4 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatRoomCreationContactData.kt +++ b/app/src/main/java/org/linphone/contact/ContactSelectionData.kt @@ -17,16 +17,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.activities.main.chat.data +package org.linphone.contact import androidx.lifecycle.MutableLiveData import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.contact.Contact -import org.linphone.contact.ContactDataInterface import org.linphone.core.* import org.linphone.utils.LinphoneUtils -class ChatRoomCreationContactData(private val searchResult: SearchResult) : ContactDataInterface { +class ContactSelectionData(private val searchResult: SearchResult) : ContactDataInterface { override val contact: MutableLiveData = MutableLiveData() override val displayName: MutableLiveData = MutableLiveData() override val securityLevel: MutableLiveData = MutableLiveData() diff --git a/app/src/main/java/org/linphone/contact/ContactsManager.kt b/app/src/main/java/org/linphone/contact/ContactsManager.kt index fdc2af0d8..7bb7658ea 100644 --- a/app/src/main/java/org/linphone/contact/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contact/ContactsManager.kt @@ -164,6 +164,7 @@ class ContactsManager(private val context: Context) { @Synchronized fun updateLocalContacts() { + Log.i("[Contacts Manager] Updating local contact(s)") localAccountsContacts.clear() for (account in coreContext.core.accountList) { @@ -171,6 +172,7 @@ class ContactsManager(private val context: Context) { localContact.fullName = account.params.identityAddress?.displayName ?: account.params.identityAddress?.username val pictureUri = corePreferences.defaultAccountAvatarPath if (pictureUri != null) { + Log.i("[Contacts Manager] Found local picture URI: $pictureUri") localContact.setContactThumbnailPictureUri(Uri.fromFile(File(pictureUri))) } val address = account.params.identityAddress diff --git a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomCreationContactsAdapter.kt b/app/src/main/java/org/linphone/contact/ContactsSelectionAdapter.kt similarity index 80% rename from app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomCreationContactsAdapter.kt rename to app/src/main/java/org/linphone/contact/ContactsSelectionAdapter.kt index aef208d05..26cf8bda1 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomCreationContactsAdapter.kt +++ b/app/src/main/java/org/linphone/contact/ContactsSelectionAdapter.kt @@ -17,8 +17,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.linphone.activities.main.chat.adapters +package org.linphone.contact +import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.ViewGroup import androidx.databinding.DataBindingUtil @@ -29,38 +30,42 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R -import org.linphone.activities.main.chat.data.ChatRoomCreationContactData import org.linphone.core.Address import org.linphone.core.FriendCapability import org.linphone.core.SearchResult -import org.linphone.databinding.ChatRoomCreationContactCellBinding +import org.linphone.databinding.ContactSelectionCellBinding import org.linphone.utils.Event -class ChatRoomCreationContactsAdapter( +class ContactsSelectionAdapter( private val viewLifecycleOwner: LifecycleOwner ) : ListAdapter(SearchResultDiffCallback()) { val selectedContact = MutableLiveData>() - var groupChatEnabled: Boolean = false - private var selectedAddresses = ArrayList
() - private var securityEnabled: Boolean = false + private var requireGroupChatCapability: Boolean = false + private var requireLimeCapability: Boolean = false + @SuppressLint("NotifyDataSetChanged") fun updateSelectedAddresses(selection: ArrayList
) { selectedAddresses = selection notifyDataSetChanged() } - fun updateSecurity(enabled: Boolean) { - securityEnabled = enabled + @SuppressLint("NotifyDataSetChanged") + fun setLimeCapabilityRequired(enabled: Boolean) { + requireLimeCapability = enabled notifyDataSetChanged() } + fun setGroupChatCapabilityRequired(enabled: Boolean) { + requireGroupChatCapability = enabled + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val binding: ChatRoomCreationContactCellBinding = DataBindingUtil.inflate( + val binding: ContactSelectionCellBinding = DataBindingUtil.inflate( LayoutInflater.from(parent.context), - R.layout.chat_room_creation_contact_cell, parent, false + R.layout.contact_selection_cell, parent, false ) return ViewHolder(binding) } @@ -70,16 +75,16 @@ class ChatRoomCreationContactsAdapter( } inner class ViewHolder( - private val binding: ChatRoomCreationContactCellBinding + private val binding: ContactSelectionCellBinding ) : RecyclerView.ViewHolder(binding.root) { fun bind(searchResult: SearchResult) { with(binding) { - val searchResultViewModel = ChatRoomCreationContactData(searchResult) + val searchResultViewModel = ContactSelectionData(searchResult) data = searchResultViewModel lifecycleOwner = viewLifecycleOwner - updateSecurity(searchResult, searchResultViewModel, securityEnabled) + updateSecurity(searchResult, searchResultViewModel, requireLimeCapability) val selected = selectedAddresses.find { address -> val searchAddress = searchResult.address @@ -97,13 +102,13 @@ class ChatRoomCreationContactsAdapter( private fun updateSecurity( searchResult: SearchResult, - viewModel: ChatRoomCreationContactData, + viewModel: ContactSelectionData, securityEnabled: Boolean ) { val searchAddress = searchResult.address val isMyself = securityEnabled && searchAddress != null && coreContext.core.defaultAccount?.params?.identityAddress?.weakEqual(searchAddress) ?: false val limeCheck = !securityEnabled || (securityEnabled && searchResult.hasCapability(FriendCapability.LimeX3Dh)) - val groupCheck = !groupChatEnabled || (groupChatEnabled && searchResult.hasCapability(FriendCapability.GroupChat)) + val groupCheck = !requireGroupChatCapability || (requireGroupChatCapability && searchResult.hasCapability(FriendCapability.GroupChat)) val disabled = if (searchResult.friend != null) !limeCheck || !groupCheck || isMyself else false // Generated entry from search filter viewModel.isDisabled.value = disabled diff --git a/app/src/main/java/org/linphone/contact/ContactsSelectionViewModel.kt b/app/src/main/java/org/linphone/contact/ContactsSelectionViewModel.kt new file mode 100644 index 000000000..ad47ee550 --- /dev/null +++ b/app/src/main/java/org/linphone/contact/ContactsSelectionViewModel.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2010-2021 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.contact + +import androidx.lifecycle.MutableLiveData +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.activities.main.viewmodels.MessageNotifierViewModel +import org.linphone.core.Address +import org.linphone.core.SearchResult +import org.linphone.core.tools.Log + +open class ContactsSelectionViewModel : MessageNotifierViewModel() { + val contactsList = MutableLiveData>() + + val sipContactsSelected = MutableLiveData() + + val selectedAddresses = MutableLiveData>() + + val filter = MutableLiveData() + private var previousFilter = "" + + private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() { + override fun onContactsUpdated() { + Log.i("[Contacts Selection] Contacts have changed") + updateContactsList() + } + } + + init { + sipContactsSelected.value = coreContext.contactsManager.shouldDisplaySipContactsList() + + selectedAddresses.value = arrayListOf() + + coreContext.contactsManager.addListener(contactsUpdatedListener) + } + + override fun onCleared() { + coreContext.contactsManager.removeListener(contactsUpdatedListener) + + super.onCleared() + } + + fun applyFilter() { + val filterValue = filter.value.orEmpty() + if (previousFilter == filterValue) return + + if (previousFilter.isNotEmpty() && previousFilter.length > filterValue.length) { + coreContext.contactsManager.magicSearch.resetSearchCache() + } + previousFilter = filterValue + + updateContactsList() + } + + fun updateContactsList() { + val domain = if (sipContactsSelected.value == true) coreContext.core.defaultAccount?.params?.domain ?: "" else "" + val results = coreContext.contactsManager.magicSearch.getContactListFromFilter(filter.value.orEmpty(), domain) + + val list = arrayListOf() + for (result in results) { + list.add(result) + } + contactsList.value = list + } + + fun toggleSelectionForSearchResult(searchResult: SearchResult) { + val address = searchResult.address + if (address != null) { + toggleSelectionForAddress(address) + } + } + + fun toggleSelectionForAddress(address: Address) { + val list = arrayListOf
() + list.addAll(selectedAddresses.value.orEmpty()) + + val found = list.find { + it.weakEqual(address) + } + + if (found != null) { + list.remove(found) + } else { + val contact = coreContext.contactsManager.findContactByAddress(address) + if (contact != null) { + val clone = address.clone() + clone.displayName = contact.fullName + list.add(clone) + } else { + list.add(address) + } + } + + selectedAddresses.value = list + } +} diff --git a/app/src/main/java/org/linphone/contact/NativeContact.kt b/app/src/main/java/org/linphone/contact/NativeContact.kt index c28f6fd6b..ff7a38c7e 100644 --- a/app/src/main/java/org/linphone/contact/NativeContact.kt +++ b/app/src/main/java/org/linphone/contact/NativeContact.kt @@ -70,7 +70,7 @@ class NativeContact(val nativeId: String, private val lookupKey: String? = null) val icon = if (bm == null) IconCompat.createWithResource( coreContext.context, - R.drawable.avatar + R.drawable.voip_single_contact_avatar ) else IconCompat.createWithAdaptiveBitmap(bm) if (icon != null) { personBuilder.setIcon(icon) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index d098d076d..99b216b69 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -51,9 +51,6 @@ import kotlinx.coroutines.* import org.linphone.BuildConfig import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R -import org.linphone.activities.call.CallActivity -import org.linphone.activities.call.IncomingCallActivity -import org.linphone.activities.call.OutgoingCallActivity import org.linphone.compatibility.Compatibility import org.linphone.compatibility.PhoneStateInterface import org.linphone.contact.Contact @@ -164,7 +161,11 @@ class CoreContext(val context: Context, coreConfig: Config) { } } } else if (state == Call.State.OutgoingInit) { - onOutgoingStarted() + val conferenceInfo = core.findConferenceInformationFromUri(call.remoteAddress) + // Do not show outgoing call view for conference calls, wait for connected state + if (conferenceInfo == null) { + onOutgoingStarted() + } } else if (state == Call.State.OutgoingProgress) { if (core.callsNb == 1 && corePreferences.routeAudioToBluetoothIfAvailable) { AudioRouteUtils.routeAudioToBluetooth(call) @@ -188,25 +189,10 @@ class CoreContext(val context: Context, coreConfig: Config) { } } } - - if (corePreferences.routeAudioToSpeakerWhenVideoIsEnabled && call.currentParams.isVideoEnabled) { - // Do not turn speaker on when video is enabled if headset or bluetooth is used - if (!AudioRouteUtils.isHeadsetAudioRouteAvailable() && !AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed( - call - ) - ) { - Log.i("[Context] Video enabled and no wired headset not bluetooth in use, routing audio to speaker") - AudioRouteUtils.routeAudioToSpeaker(call) - } - } } else if (state == Call.State.End || state == Call.State.Error || state == Call.State.Released) { - if (core.callsNb == 0) { - removeCallOverlay() - } - if (state == Call.State.Error) { Log.w("[Context] Call error reason is ${call.errorInfo.protocolCode} / ${call.errorInfo.reason} / ${call.errorInfo.phrase}") - val message = when (call.errorInfo.reason) { + val toastMessage = when (call.errorInfo.reason) { Reason.Busy -> context.getString(R.string.call_error_user_busy) Reason.IOError -> context.getString(R.string.call_error_io_error) Reason.NotAcceptable -> context.getString(R.string.call_error_incompatible_media_params) @@ -215,20 +201,26 @@ class CoreContext(val context: Context, coreConfig: Config) { Reason.TemporarilyUnavailable -> context.getString(R.string.call_error_temporarily_unavailable) else -> context.getString(R.string.call_error_generic).format("${call.errorInfo.protocolCode} / ${call.errorInfo.phrase}") } - callErrorMessageResourceId.value = Event(message) + callErrorMessageResourceId.value = Event(toastMessage) } else if (state == Call.State.End && call.dir == Call.Dir.Outgoing && - call.errorInfo.reason == Reason.Declined + call.errorInfo.reason == Reason.Declined && + core.callsNb == 0 ) { Log.i("[Context] Call has been declined") - val message = context.getString(R.string.call_error_declined) - callErrorMessageResourceId.value = Event(message) + val toastMessage = context.getString(R.string.call_error_declined) + callErrorMessageResourceId.value = Event(toastMessage) } } previousCallState = state } + override fun onLastCallEnded(core: Core) { + Log.i("[Context] Last call has ended") + removeCallOverlay() + } + override fun onMessageReceived(core: Core, chatRoom: ChatRoom, message: ChatMessage) { if (core.maxSizeForAutoDownloadIncomingFiles != -1) { var hasFile = false @@ -267,7 +259,8 @@ class CoreContext(val context: Context, coreConfig: Config) { init { if (context.resources.getBoolean(R.bool.crashlytics_enabled)) { - loggingService.addListener(loggingServiceListener) + // TODO: FIXME: uncomment + // loggingService.addListener(loggingServiceListener) Log.i("[Context] Crashlytics enabled, register logging service listener") } @@ -347,7 +340,8 @@ class CoreContext(val context: Context, coreConfig: Config) { core.stop() core.removeListener(listener) stopped = true - loggingService.removeListener(loggingServiceListener) + // TODO: FIXME: uncomment + // loggingService.removeListener(loggingServiceListener) } private fun configureCore() { @@ -367,12 +361,46 @@ class CoreContext(val context: Context, coreConfig: Config) { for (account in core.accountList) { if (account.params.identityAddress?.domain == corePreferences.defaultDomain) { - // Ensure conference URI is set on sip.linphone.org proxy configs + var paramsChanged = false + val params = account.params.clone() + + // Ensure conference factory URI is set on sip.linphone.org proxy configs if (account.params.conferenceFactoryUri == null) { - val params = account.params.clone() val uri = corePreferences.conferenceServerUri Log.i("[Context] Setting conference factory on proxy config ${params.identityAddress?.asString()} to default value: $uri") params.conferenceFactoryUri = uri + paramsChanged = true + } + + // Ensure audio/video conference factory URI is set on sip.linphone.org proxy configs + if (account.params.audioVideoConferenceFactoryAddress == null) { + val uri = corePreferences.audioVideoConferenceServerUri + val address = core.interpretUrl(uri) + if (address != null) { + Log.i("[Context] Setting audio/video conference factory on proxy config ${params.identityAddress?.asString()} to default value: $uri") + params.audioVideoConferenceFactoryAddress = address + paramsChanged = true + } else { + Log.e("[Context] Failed to parse audio/video conference factory URI: $uri") + } + } + + // Enable Bundle mode by default + if (!account.params.isRtpBundleEnabled) { + Log.i("[Context] Enabling RTP bundle mode on proxy config ${params.identityAddress?.asString()}") + params.isRtpBundleEnabled = true + paramsChanged = true + } + + // Ensure we allow CPIM messages in basic chat rooms + if (!account.params.isCpimInBasicChatRoomEnabled) { + params.isCpimInBasicChatRoomEnabled = true + paramsChanged = true + Log.i("[Context] CPIM allowed in basic chat rooms for account ${params.identityAddress?.asString()}") + } + + if (paramsChanged) { + Log.i("[Context] Account params have been updated, apply changes") account.params = params } @@ -385,12 +413,6 @@ class CoreContext(val context: Context, coreConfig: Config) { core.limeX3DhServerUrl = url } } - - // Ensure we allow CPIM messages in basic chat rooms - val newParams = account.params.clone() - newParams.isCpimInBasicChatRoomEnabled = true - account.params = newParams - Log.i("[Context] CPIM allowed in basic chat rooms for account ${newParams.identityAddress?.asStringUriOnly()}") } } @@ -462,6 +484,13 @@ class CoreContext(val context: Context, coreConfig: Config) { return false } + fun videoUpdateRequestTimedOut(call: Call) { + coroutineScope.launch { + Log.w("[Context] 30 seconds have passed, declining video request") + answerCallVideoUpdateRequest(call, false) + } + } + fun answerCallVideoUpdateRequest(call: Call, accept: Boolean) { val params = core.createCallParams(call) @@ -506,7 +535,7 @@ class CoreContext(val context: Context, coreConfig: Config) { call.terminate() } - fun transferCallTo(addressToCall: String) { + fun transferCallTo(addressToCall: String): Boolean { val currentCall = core.currentCall ?: core.calls.firstOrNull() if (currentCall == null) { Log.e("[Context] Couldn't find a call to transfer") @@ -515,8 +544,10 @@ class CoreContext(val context: Context, coreConfig: Config) { if (address != null) { Log.i("[Context] Transferring current call to $addressToCall") currentCall.transferTo(address) + return true } } + return false } fun startCall(to: String) { @@ -540,14 +571,19 @@ class CoreContext(val context: Context, coreConfig: Config) { startCall(address) } - fun startCall(address: Address, forceZRTP: Boolean = false, localAddress: Address? = null) { + fun startCall( + address: Address, + callParams: CallParams? = null, + forceZRTP: Boolean = false, + localAddress: Address? = null + ) { if (!core.isNetworkReachable) { Log.e("[Context] Network unreachable, abort outgoing call") callErrorMessageResourceId.value = Event(context.getString(R.string.call_error_network_unreachable)) return } - val params = core.createCallParams(null) + val params = callParams ?: core.createCallParams(null) if (params == null) { val call = core.inviteAddress(address) Log.w("[Context] Starting call $call without params") @@ -607,15 +643,6 @@ class CoreContext(val context: Context, coreConfig: Config) { return core.videoDevicesList.size > 2 // Count StaticImage camera } - fun isVideoCallOrConferenceActive(): Boolean { - val conference = core.conference - return if (conference != null && conference.isIn) { - conference.currentParams.isVideoEnabled - } else { - core.currentCall?.currentParams?.isVideoEnabled ?: false - } - } - fun createCallOverlay() { if (!corePreferences.showCallOverlay || !corePreferences.systemWideCallOverlay || callOverlay != null) { return @@ -787,9 +814,9 @@ class CoreContext(val context: Context, coreConfig: Config) { } Log.i("[Context] Starting IncomingCallActivity") - val intent = Intent(context, IncomingCallActivity::class.java) + val intent = Intent(context, org.linphone.activities.voip.CallActivity::class.java) // This flag is required to start an Activity from a Service context - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) context.startActivity(intent) } @@ -800,9 +827,9 @@ class CoreContext(val context: Context, coreConfig: Config) { } Log.i("[Context] Starting OutgoingCallActivity") - val intent = Intent(context, OutgoingCallActivity::class.java) + val intent = Intent(context, org.linphone.activities.voip.CallActivity::class.java) // This flag is required to start an Activity from a Service context - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) context.startActivity(intent) } @@ -813,7 +840,7 @@ class CoreContext(val context: Context, coreConfig: Config) { } Log.i("[Context] Starting CallActivity") - val intent = Intent(context, CallActivity::class.java) + val intent = Intent(context, org.linphone.activities.voip.CallActivity::class.java) // This flag is required to start an Activity from a Service context intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) context.startActivity(intent) diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index e69d23f9b..641694af1 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -324,12 +324,6 @@ class CorePreferences constructor(private val context: Context) { config.setBool("app", "user_disabled_self_managed_telecom_manager", value) } - var fullScreenCallUI: Boolean - get() = config.getBool("app", "full_screen_call", false) - set(value) { - config.setBool("app", "full_screen_call", value) - } - var routeAudioToBluetoothIfAvailable: Boolean get() = config.getBool("app", "route_audio_to_bluetooth_if_available", true) set(value) { @@ -350,6 +344,12 @@ class CorePreferences constructor(private val context: Context) { config.setBool("audio", "android_pause_calls_when_audio_focus_lost", value) } + var enableFullScreenWhenJoiningVideoConference: Boolean + get() = config.getBool("app", "enter_video_conference_enable_full_screen_mode", true) + set(value) { + config.setBool("app", "enter_video_conference_enable_full_screen_mode", value) + } + /* Assistant */ var firstStart: Boolean @@ -443,11 +443,6 @@ class CorePreferences constructor(private val context: Context) { val disableChat: Boolean get() = config.getBool("app", "disable_chat_feature", false) - // If enabled, this will cause the video to "freeze" on your correspondent screen - // as you won't send video packets anymore - val hideCameraPreviewInPipMode: Boolean - get() = config.getBool("app", "hide_camera_preview_in_pip_mode", false) - // This will prevent UI from showing up, except for the launcher & the foreground service notification val preventInterfaceFromShowingUp: Boolean get() = config.getBool("app", "keep_app_invisible", false) @@ -479,11 +474,21 @@ class CorePreferences constructor(private val context: Context) { val debugPopupCode: String get() = config.getString("app", "debug_popup_magic", "#1234#")!! + // If there is more participants than this value in a conference, force ActiveSpeaker layout + val maxConferenceParticipantsForMosaicLayout: Int = 6 + val conferenceServerUri: String get() = config.getString( "app", "default_conference_factory_uri", - "sip:conference-factory@sip.linphone.org" + "" + )!! + + val audioVideoConferenceServerUri: String + get() = config.getString( + "app", + "default_audio_video_conference_factory_uri", + "" )!! val limeX3dhServerUrl: String @@ -531,6 +536,9 @@ class CorePreferences constructor(private val context: Context) { val showRecordingsInSideMenu: Boolean get() = config.getBool("app", "side_menu_recordings", true) + val showScheduledConferencesInSideMenu: Boolean + get() = config.getBool("app", "side_menu_conferences", true) + val showAboutInSideMenu: Boolean get() = config.getBool("app", "side_menu_about", true) @@ -569,6 +577,9 @@ class CorePreferences constructor(private val context: Context) { val showAdvancedSettings: Boolean get() = config.getBool("app", "settings_advanced", true) + val showConferencesSettings: Boolean + get() = config.getBool("app", "settings_conferences", true) + /* Assets stuff */ val configPath: String diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 4df57605a..8a415fa9d 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -41,11 +41,9 @@ import kotlin.collections.HashMap import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R -import org.linphone.activities.call.CallActivity -import org.linphone.activities.call.IncomingCallActivity -import org.linphone.activities.call.OutgoingCallActivity import org.linphone.activities.chat_bubble.ChatBubbleActivity import org.linphone.activities.main.MainActivity +import org.linphone.activities.voip.CallActivity import org.linphone.compatibility.Compatibility import org.linphone.contact.Contact import org.linphone.core.* @@ -426,7 +424,7 @@ class NotificationsManager(private val context: Context) { if (picture != null) { IconCompat.createWithAdaptiveBitmap(picture) } else { - IconCompat.createWithResource(context, R.drawable.avatar) + IconCompat.createWithResource(context, R.drawable.voip_single_contact_avatar) } if (userIcon != null) builder.setIcon(userIcon) builder.build() @@ -446,7 +444,7 @@ class NotificationsManager(private val context: Context) { currentForegroundServiceNotificationId = 0 } - val incomingCallNotificationIntent = Intent(context, IncomingCallActivity::class.java) + val incomingCallNotificationIntent = Intent(context, org.linphone.activities.voip.CallActivity::class.java) incomingCallNotificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) val pendingIntent = PendingIntent.getActivity( context, @@ -514,18 +512,6 @@ class NotificationsManager(private val context: Context) { fun displayCallNotification(call: Call, useAsForeground: Boolean = false) { val notifiable = getNotifiableForCall(call) - val callActivity: Class<*> = when (call.state) { - Call.State.Paused, Call.State.Pausing, Call.State.PausedByRemote -> { - CallActivity::class.java - } - Call.State.OutgoingRinging, Call.State.OutgoingProgress, Call.State.OutgoingInit, Call.State.OutgoingEarlyMedia -> { - OutgoingCallActivity::class.java - } - else -> { - CallActivity::class.java - } - } - val serviceChannel = context.getString(R.string.notification_channel_service_id) val channelToUse = when (val serviceChannelImportance = Compatibility.getChannelImportance(notificationManager, serviceChannel)) { NotificationManagerCompat.IMPORTANCE_NONE -> { @@ -543,7 +529,7 @@ class NotificationsManager(private val context: Context) { } } - val callNotificationIntent = Intent(context, callActivity) + val callNotificationIntent = Intent(context, CallActivity::class.java) callNotificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) val pendingIntent = PendingIntent.getActivity( context, @@ -651,8 +637,15 @@ class NotificationsManager(private val context: Context) { val pictureUri = contact?.getContactThumbnailPictureUri() val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri) val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(message.fromAddress) + var text = "" + + val isConferenceInvite = message.contents.firstOrNull()?.isIcalendar ?: false + text = if (isConferenceInvite) { + AppUtils.getString(R.string.conference_invitation_received_notification) + } else { + message.contents.find { content -> content.isText }?.utf8Text ?: "" + } - var text: String = message.contents.find { content -> content.isText }?.utf8Text ?: "" if (text.isEmpty()) { for (content in message.contents) { text += content.name @@ -781,7 +774,7 @@ class NotificationsManager(private val context: Context) { } style.isGroupConversation = notifiable.isGroup - val icon = lastPerson?.icon ?: IconCompat.createWithResource(context, R.drawable.avatar) + val icon = lastPerson?.icon ?: IconCompat.createWithResource(context, R.drawable.voip_single_contact_avatar) val bubble = NotificationCompat.BubbleMetadata.Builder(bubbleIntent, icon) .setDesiredHeightResId(R.dimen.chat_message_bubble_desired_height) .build() diff --git a/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt b/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt index 2173faff3..a93bfd003 100644 --- a/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt +++ b/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt @@ -44,7 +44,7 @@ class NativeCallWrapper(var callId: String) : Connection() { } override fun onStateChanged(state: Int) { - Log.i("[Connection] Telecom state changed [$state] for call with id: $callId") + Log.i("[Connection] Telecom state changed [${intStateToString(state)}] for call with id: $callId") super.onStateChanged(state) } @@ -70,6 +70,11 @@ class NativeCallWrapper(var callId: String) : Connection() { val call = getCall() if (call != null) { + if (getState() != STATE_ACTIVE) { + Log.w("[Connection] Call state isn't STATE_ACTIVE, ignoring mute mic & audio route directive from TelecomManager") + return + } + call.microphoneMuted = state.isMuted when (state.route) { CallAudioState.ROUTE_EARPIECE -> AudioRouteUtils.routeAudioToEarpiece(call, true) @@ -118,4 +123,18 @@ class NativeCallWrapper(var callId: String) : Connection() { destroy() } } + + private fun intStateToString(state: Int): String { + return when (state) { + STATE_INITIALIZING -> "STATE_INITIALIZING" + STATE_NEW -> "STATE_NEW" + STATE_RINGING -> "STATE_RINGING" + STATE_DIALING -> "STATE_DIALING" + STATE_ACTIVE -> "STATE_ACTIVE" + STATE_HOLDING -> "STATE_HOLDING" + STATE_DISCONNECTED -> "STATE_DISCONNECTED" + STATE_PULLING_CALL -> "STATE_PULLING_CALL" + else -> "STATE_UNKNOWN" + } + } } diff --git a/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt b/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt index d23b9759b..704c6b306 100644 --- a/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt +++ b/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt @@ -46,11 +46,12 @@ class AudioRouteUtils { } val typesNames = stringBuilder.toString() - if (coreContext.core.callsNb == 0) { - Log.e("[Audio Route Helper] No call found, aborting [$typesNames] audio route change") - return + val currentCall = if (coreContext.core.callsNb > 0) { + call ?: coreContext.core.currentCall ?: coreContext.core.calls[0] + } else { + Log.w("[Audio Route Helper] No call found, setting audio route on Core") + null } - val currentCall = call ?: coreContext.core.currentCall ?: coreContext.core.calls[0] val conference = coreContext.core.conference val capability = if (output) AudioDevice.Capabilities.CapabilityPlay @@ -63,10 +64,14 @@ class AudioRouteUtils { Log.i("[Audio Route Helper] Found [${audioDevice.type}] ${if (output) "playback" else "recorder"} audio device [${audioDevice.deviceName}], routing conference audio to it") if (output) conference.outputAudioDevice = audioDevice else conference.inputAudioDevice = audioDevice - } else { + } else if (currentCall != null) { Log.i("[Audio Route Helper] Found [${audioDevice.type}] ${if (output) "playback" else "recorder"} audio device [${audioDevice.deviceName}], routing call audio to it") if (output) currentCall.outputAudioDevice = audioDevice else currentCall.inputAudioDevice = audioDevice + } else { + Log.i("[Audio Route Helper] Found [${audioDevice.type}] ${if (output) "playback" else "recorder"} audio device [${audioDevice.deviceName}], changing core default audio device") + if (output) coreContext.core.outputAudioDevice = audioDevice + else coreContext.core.inputAudioDevice = audioDevice } return } @@ -96,11 +101,10 @@ class AudioRouteUtils { types: List, skipTelecom: Boolean = false ) { - val currentCall = call ?: coreContext.core.currentCall ?: coreContext.core.calls[0] - if ((call != null || currentCall != null) && !skipTelecom && TelecomHelper.exists()) { - val callToUse = call ?: currentCall + val currentCall = call ?: coreContext.core.currentCall ?: if (coreContext.core.callsNb > 0) coreContext.core.calls[0] else null + if (currentCall != null && !skipTelecom && TelecomHelper.exists()) { Log.i("[Audio Route Helper] Call provided & Telecom Helper exists, trying to dispatch audio route change through Telecom API") - val connection = TelecomHelper.get().findConnectionForCallId(callToUse.callLog.callId) + val connection = TelecomHelper.get().findConnectionForCallId(currentCall.callLog.callId) if (connection != null) { val route = when (types.first()) { AudioDevice.Type.Earpiece -> CallAudioState.ROUTE_EARPIECE @@ -114,13 +118,13 @@ class AudioRouteUtils { // but this time with skipTelecom = true if (!Compatibility.changeAudioRouteForTelecomManager(connection, route)) { Log.w("[Audio Route Helper] Connection is already using this route internally, make the change!") - applyAudioRouteChange(callToUse, types) - changeCaptureDeviceToMatchAudioRoute(callToUse, types) + applyAudioRouteChange(currentCall, types) + changeCaptureDeviceToMatchAudioRoute(currentCall, types) } } else { Log.w("[Audio Route Helper] Telecom Helper found but no matching connection!") - applyAudioRouteChange(callToUse, types) - changeCaptureDeviceToMatchAudioRoute(callToUse, types) + applyAudioRouteChange(currentCall, types) + changeCaptureDeviceToMatchAudioRoute(currentCall, types) } } else { applyAudioRouteChange(call, types) @@ -145,15 +149,21 @@ class AudioRouteUtils { } fun isSpeakerAudioRouteCurrentlyUsed(call: Call? = null): Boolean { - if (coreContext.core.callsNb == 0) { - Log.w("[Audio Route Helper] No call found, so speaker audio route isn't used") - return false + val currentCall = if (coreContext.core.callsNb > 0) { + call ?: coreContext.core.currentCall ?: coreContext.core.calls[0] + } else { + Log.w("[Audio Route Helper] No call found, checking audio route on Core") + null } - val currentCall = call ?: coreContext.core.currentCall ?: coreContext.core.calls[0] val conference = coreContext.core.conference - val audioDevice = if (conference != null && conference.isIn) conference.outputAudioDevice else currentCall.outputAudioDevice - Log.i("[Audio Route Helper] Playback audio device currently in use is [${audioDevice?.deviceName}] with type (${audioDevice?.type})") + val audioDevice = if (conference != null && conference.isIn) + conference.outputAudioDevice + else if (currentCall != null) + currentCall.outputAudioDevice + else + coreContext.core.outputAudioDevice + Log.i("[Audio Route Helper] Playback audio currently in use is [${audioDevice?.deviceName}] with type (${audioDevice?.type})") return audioDevice?.type == AudioDevice.Type.Speaker } diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 62e2cfaf4..bf7778b6b 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -28,6 +28,7 @@ import android.text.Editable import android.text.TextWatcher import android.util.Patterns import android.view.LayoutInflater +import android.view.TextureView import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo @@ -51,6 +52,8 @@ import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.activities.GenericActivity import org.linphone.activities.main.settings.SettingListener +import org.linphone.activities.voip.data.ConferenceParticipantDeviceData +import org.linphone.activities.voip.views.HorizontalScrollDotsView import org.linphone.contact.ContactAvatarView import org.linphone.core.tools.Log import org.linphone.views.VoiceRecordProgressBar @@ -100,8 +103,8 @@ fun View.setLayoutSize(dimension: Float) { this.layoutParams.width = dimension.toInt() } -@BindingAdapter("android:background") -fun LinearLayout.setBackground(resource: Int) { +@BindingAdapter("backgroundImage") +fun LinearLayout.setBackgroundImage(resource: Int) { this.setBackgroundResource(resource) } @@ -326,7 +329,7 @@ fun loadAvatarWithGlideFallback(imageView: ImageView, path: String?) { .into(imageView) } else { Log.w("[Data Binding] [Glide] Can't load $path") - imageView.setImageResource(R.drawable.avatar) + imageView.setImageResource(R.drawable.voip_single_contact_avatar) } } @@ -355,6 +358,7 @@ fun loadAvatarWithGlide(imageView: ImageView, path: Uri?) { @BindingAdapter("glideAvatar") fun loadAvatarWithGlide(imageView: ImageView, path: String?) { if (path != null) { + imageView.visibility = View.VISIBLE GlideApp .with(imageView) .load(path) @@ -573,3 +577,75 @@ fun VoiceRecordProgressBar.setSecProgress(progress: Int) { fun VoiceRecordProgressBar.setSecProgressTint(color: Int) { setSecondaryProgressTint(color) } + +@BindingAdapter("android:layout_margin") +fun ConstraintLayout.setMargins(margins: Float) { + val params = layoutParams as ConstraintLayout.LayoutParams + val m = margins.toInt() + params.setMargins(m, m, m, m) + layoutParams = params +} + +@BindingAdapter("android:onTouch") +fun View.setTouchListener(listener: View.OnTouchListener) { + setOnTouchListener(listener) +} + +@BindingAdapter("entries") +fun Spinner.setEntries(entries: List?) { + if (entries != null) { + val arrayAdapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, entries) + arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + adapter = arrayAdapter + } +} + +@BindingAdapter("selectedValueAttrChanged") +fun Spinner.setInverseBindingListener(listener: InverseBindingListener) { + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { + if (tag != position) { + listener.onChange() + } + } + + override fun onNothingSelected(parent: AdapterView<*>) {} + } +} + +@BindingAdapter("selectedValue") +fun Spinner.setSelectedValue(value: Any?) { + if (adapter != null) { + val position = (adapter as ArrayAdapter).getPosition(value) + setSelection(position, false) + tag = position + } +} + +@InverseBindingAdapter(attribute = "selectedValue", event = "selectedValueAttrChanged") +fun Spinner.getSelectedValue(): Any? { + return selectedItem +} + +@BindingAdapter("participantTextureView") +fun setParticipantTextureView( + textureView: TextureView, + conferenceParticipantData: ConferenceParticipantDeviceData +) { + conferenceParticipantData.setTextureView(textureView) +} + +@BindingAdapter("app:dotCount") +fun HorizontalScrollDotsView.setDots(count: Int) { + setDotCount(count) +} + +@BindingAdapter("app:itemCount") +fun HorizontalScrollDotsView.setItems(count: Int) { + setItemCount(count) +} + +@BindingAdapter("app:selectedDot") +fun HorizontalScrollDotsView.setSelectedIndex(index: Int) { + setSelectedDot(index) +} diff --git a/app/src/main/java/org/linphone/utils/DialogUtils.kt b/app/src/main/java/org/linphone/utils/DialogUtils.kt index 5f27b4b83..8c7fe46cf 100644 --- a/app/src/main/java/org/linphone/utils/DialogUtils.kt +++ b/app/src/main/java/org/linphone/utils/DialogUtils.kt @@ -31,6 +31,7 @@ import androidx.databinding.DataBindingUtil import org.linphone.R import org.linphone.activities.main.viewmodels.DialogViewModel import org.linphone.databinding.DialogBinding +import org.linphone.databinding.VoipDialogBinding class DialogUtils { companion object { @@ -52,5 +53,24 @@ class DialogUtils { dialog.window?.setBackgroundDrawable(d) return dialog } + + fun getVoipDialog(context: Context, viewModel: DialogViewModel): Dialog { + val dialog = Dialog(context, R.style.AppTheme) + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE) + + val binding: VoipDialogBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.voip_dialog, null, false) + binding.viewModel = viewModel + dialog.setContentView(binding.root) + + val d: Drawable = ColorDrawable(ContextCompat.getColor(dialog.context, R.color.voip_dark_gray)) + d.alpha = 166 + dialog.window + ?.setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT + ) + dialog.window?.setBackgroundDrawable(d) + return dialog + } } } diff --git a/app/src/main/java/org/linphone/utils/FileUtils.kt b/app/src/main/java/org/linphone/utils/FileUtils.kt index 30adf0ba1..665827373 100644 --- a/app/src/main/java/org/linphone/utils/FileUtils.kt +++ b/app/src/main/java/org/linphone/utils/FileUtils.kt @@ -316,7 +316,12 @@ class FileUtils { val nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) if (nameIndex != -1) { try { - name = returnCursor.getString(nameIndex) + val displayName = returnCursor.getString(nameIndex) + if (displayName != null) { + name = displayName + } else { + Log.e("[File Utils] Failed to get the display name for URI $uri, returned value is null") + } } catch (e: CursorIndexOutOfBoundsException) { Log.e("[File Utils] Failed to get the display name for URI $uri, exception is $e") } @@ -513,5 +518,20 @@ class FileUtils { } return false } + + fun writeIntoFile(bytes: ByteArray, file: File) { + val inStream = ByteArrayInputStream(bytes) + val outStream = FileOutputStream(file) + + val buffer = ByteArray(1024) + var read: Int + while (inStream.read(buffer).also { read = it } != -1) { + outStream.write(buffer, 0, read) + } + + inStream.close() + outStream.flush() + outStream.close() + } } } diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 162811c99..6bea3de9e 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -40,7 +40,8 @@ class LinphoneUtils { companion object { private const val RECORDING_DATE_PATTERN = "dd-MM-yyyy-HH-mm-ss" - fun getDisplayName(address: Address): String { + fun getDisplayName(address: Address?): String { + if (address == null) return "[null]" if (address.displayName == null) { val account = coreContext.core.accountList.find { account -> account.params.identityAddress?.asStringUriOnly() == address.asStringUriOnly() @@ -76,6 +77,11 @@ class LinphoneUtils { return core.defaultAccount?.params?.conferenceFactoryUri != null } + fun isRemoteConferencingAvailable(): Boolean { + val core = coreContext.core + return core.defaultAccount?.params?.audioVideoConferenceFactoryAddress != null || core.defaultAccount?.params?.conferenceFactoryUri != null + } + fun createOneToOneChatRoom(participant: Address, isSecured: Boolean = false): ChatRoom? { val core: Core = coreContext.core val defaultAccount = core.defaultAccount diff --git a/app/src/main/java/org/linphone/utils/ShortcutsHelper.kt b/app/src/main/java/org/linphone/utils/ShortcutsHelper.kt index c02c25fed..8a30bb0f6 100644 --- a/app/src/main/java/org/linphone/utils/ShortcutsHelper.kt +++ b/app/src/main/java/org/linphone/utils/ShortcutsHelper.kt @@ -169,7 +169,7 @@ class ShortcutsHelper(val context: Context) { personsList.add(contact.getPerson()) } subject = contact?.fullName ?: LinphoneUtils.getDisplayName(chatRoom.peerAddress) - icon = contact?.getPerson()?.icon ?: IconCompat.createWithResource(context, R.drawable.avatar) + icon = contact?.getPerson()?.icon ?: IconCompat.createWithResource(context, R.drawable.voip_single_contact_avatar) } else if (chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) && chatRoom.participants.isNotEmpty()) { val address = chatRoom.participants.first().address val contact = @@ -178,7 +178,7 @@ class ShortcutsHelper(val context: Context) { personsList.add(contact.getPerson()) } subject = contact?.fullName ?: LinphoneUtils.getDisplayName(address) - icon = contact?.getPerson()?.icon ?: IconCompat.createWithResource(context, R.drawable.avatar) + icon = contact?.getPerson()?.icon ?: IconCompat.createWithResource(context, R.drawable.voip_single_contact_avatar) } else { for (participant in chatRoom.participants) { val contact = @@ -188,7 +188,7 @@ class ShortcutsHelper(val context: Context) { } } subject = chatRoom.subject.orEmpty() - icon = IconCompat.createWithResource(context, R.drawable.chat_group_avatar) + icon = IconCompat.createWithResource(context, R.drawable.voip_multiple_contacts_avatar) } val persons = arrayOfNulls(personsList.size) diff --git a/app/src/main/java/org/linphone/utils/TimestampUtils.kt b/app/src/main/java/org/linphone/utils/TimestampUtils.kt index 20e269448..c4998395c 100644 --- a/app/src/main/java/org/linphone/utils/TimestampUtils.kt +++ b/app/src/main/java/org/linphone/utils/TimestampUtils.kt @@ -20,8 +20,10 @@ package org.linphone.utils import java.text.DateFormat +import java.text.Format import java.text.SimpleDateFormat import java.util.* +import org.linphone.LinphoneApplication class TimestampUtils { companion object { @@ -54,6 +56,56 @@ class TimestampUtils { return isSameDay(cal1.time, cal2.time, false) } + fun dateToString(date: Long, timestampInSecs: Boolean = true): String { + val dateFormat: Format = android.text.format.DateFormat.getDateFormat( + LinphoneApplication.coreContext.context + ) + val pattern = (dateFormat as SimpleDateFormat).toLocalizedPattern() + + val calendar = Calendar.getInstance() + calendar.timeInMillis = if (timestampInSecs) date * 1000 else date + return SimpleDateFormat(pattern, Locale.getDefault()).format(calendar.time) + } + + fun timeToString(hour: Int, minutes: Int): String { + val use24hFormat = android.text.format.DateFormat.is24HourFormat(LinphoneApplication.coreContext.context) + val calendar = Calendar.getInstance() + calendar.set(Calendar.HOUR_OF_DAY, hour) + calendar.set(Calendar.MINUTE, minutes) + + return if (use24hFormat) { + SimpleDateFormat("HH'h'mm", Locale.getDefault()).format(calendar.time) + } else { + SimpleDateFormat("h:mm a", Locale.getDefault()).format(calendar.time) + } + } + + fun timeToString(time: Long, timestampInSecs: Boolean = true): String { + val use24hFormat = android.text.format.DateFormat.is24HourFormat(LinphoneApplication.coreContext.context) + val calendar = Calendar.getInstance() + calendar.timeInMillis = if (timestampInSecs) time * 1000 else time + + return if (use24hFormat) { + SimpleDateFormat("HH'h'mm", Locale.getDefault()).format(calendar.time) + } else { + SimpleDateFormat("h:mm a", Locale.getDefault()).format(calendar.time) + } + } + + fun durationToString(hours: Int, minutes: Int): String { + val calendar = Calendar.getInstance() + calendar.set(Calendar.HOUR_OF_DAY, hours) + calendar.set(Calendar.MINUTE, minutes) + val pattern = when { + hours == 0 -> "mm'min'" + hours < 10 && minutes == 0 -> "H'h'" + hours < 10 && minutes > 0 -> "H'h'mm" + hours >= 10 && minutes == 0 -> "HH'h'" + else -> "HH'h'mm" + } + return SimpleDateFormat(pattern, Locale.getDefault()).format(calendar.time) + } + private fun isSameYear(timestamp: Long, timestampInSecs: Boolean = true): Boolean { val cal = Calendar.getInstance() cal.timeInMillis = if (timestampInSecs) timestamp * 1000 else timestamp @@ -85,7 +137,7 @@ class TimestampUtils { } val millis = if (timestampInSecs) timestamp * 1000 else timestamp - return dateFormat.format(Date(millis)) + return dateFormat.format(Date(millis)).capitalize(Locale.getDefault()) } private fun isSameDay( diff --git a/app/src/main/res/color/security_switch_track_color.xml b/app/src/main/res/color/security_switch_track_color.xml deleted file mode 100644 index 2d884e585..000000000 --- a/app/src/main/res/color/security_switch_track_color.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/voip_extra_button_text_color.xml b/app/src/main/res/color/voip_extra_button_text_color.xml new file mode 100644 index 000000000..4c7f3d593 --- /dev/null +++ b/app/src/main/res/color/voip_extra_button_text_color.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable-xhdpi/arrow_accept.png b/app/src/main/res/drawable-xhdpi/arrow_accept.png deleted file mode 100644 index c90817e48..000000000 Binary files a/app/src/main/res/drawable-xhdpi/arrow_accept.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/arrow_hangup.png b/app/src/main/res/drawable-xhdpi/arrow_hangup.png deleted file mode 100644 index 8c113903c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/arrow_hangup.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/audio.png b/app/src/main/res/drawable-xhdpi/audio.png deleted file mode 100644 index 472ae2bff..000000000 Binary files a/app/src/main/res/drawable-xhdpi/audio.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/avatar.png b/app/src/main/res/drawable-xhdpi/avatar.png deleted file mode 100644 index 6c91dd654..000000000 Binary files a/app/src/main/res/drawable-xhdpi/avatar.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/camera_default.png b/app/src/main/res/drawable-xhdpi/camera_default.png deleted file mode 100644 index 34a6c1c82..000000000 Binary files a/app/src/main/res/drawable-xhdpi/camera_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/cancel_edit_default.png b/app/src/main/res/drawable-xhdpi/cancel_edit_default.png deleted file mode 100644 index 451836e7e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/cancel_edit_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/chat_group_avatar.png b/app/src/main/res/drawable-xhdpi/chat_group_avatar.png deleted file mode 100644 index db9230dbb..000000000 Binary files a/app/src/main/res/drawable-xhdpi/chat_group_avatar.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/checkbox_checked.png b/app/src/main/res/drawable-xhdpi/checkbox_checked.png deleted file mode 100644 index 66f4ddec6..000000000 Binary files a/app/src/main/res/drawable-xhdpi/checkbox_checked.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/checkbox_unchecked.png b/app/src/main/res/drawable-xhdpi/checkbox_unchecked.png deleted file mode 100644 index f0ec61800..000000000 Binary files a/app/src/main/res/drawable-xhdpi/checkbox_unchecked.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/conference_exit_default.png b/app/src/main/res/drawable-xhdpi/conference_exit_default.png deleted file mode 100644 index ccc8a0ac7..000000000 Binary files a/app/src/main/res/drawable-xhdpi/conference_exit_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/conference_schedule_calendar_default.png b/app/src/main/res/drawable-xhdpi/conference_schedule_calendar_default.png new file mode 100644 index 000000000..59fe950fb Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/conference_schedule_calendar_default.png differ diff --git a/app/src/main/res/drawable-xhdpi/conference_schedule_participants_default.png b/app/src/main/res/drawable-xhdpi/conference_schedule_participants_default.png new file mode 100644 index 000000000..a58af55e1 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/conference_schedule_participants_default.png differ diff --git a/app/src/main/res/drawable-xhdpi/conference_schedule_time_default.png b/app/src/main/res/drawable-xhdpi/conference_schedule_time_default.png new file mode 100644 index 000000000..7a9534617 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/conference_schedule_time_default.png differ diff --git a/app/src/main/res/drawable-xhdpi/dialer_alt_back.png b/app/src/main/res/drawable-xhdpi/dialer_alt_back.png deleted file mode 100644 index 9c1f84b91..000000000 Binary files a/app/src/main/res/drawable-xhdpi/dialer_alt_back.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/download_default.png b/app/src/main/res/drawable-xhdpi/download_default.png deleted file mode 100644 index 7b0a1095d..000000000 Binary files a/app/src/main/res/drawable-xhdpi/download_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/micro_default.png b/app/src/main/res/drawable-xhdpi/micro_default.png deleted file mode 100644 index 3b3f2d958..000000000 Binary files a/app/src/main/res/drawable-xhdpi/micro_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/more_menu_default.png b/app/src/main/res/drawable-xhdpi/more_menu_default.png deleted file mode 100644 index 51162a31a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/more_menu_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/next_default.png b/app/src/main/res/drawable-xhdpi/next_default.png new file mode 100644 index 000000000..3cea216fb Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/next_default.png differ diff --git a/app/src/main/res/drawable-xhdpi/options_add_call_default.png b/app/src/main/res/drawable-xhdpi/options_add_call_default.png deleted file mode 100644 index a4b8c9ad9..000000000 Binary files a/app/src/main/res/drawable-xhdpi/options_add_call_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/options_default.png b/app/src/main/res/drawable-xhdpi/options_default.png deleted file mode 100644 index e13c5942c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/options_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/options_rec_default.png b/app/src/main/res/drawable-xhdpi/options_rec_default.png deleted file mode 100644 index 075c09314..000000000 Binary files a/app/src/main/res/drawable-xhdpi/options_rec_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/options_transfer_call_default.png b/app/src/main/res/drawable-xhdpi/options_transfer_call_default.png deleted file mode 100644 index 6b4bc1665..000000000 Binary files a/app/src/main/res/drawable-xhdpi/options_transfer_call_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/pause_default.png b/app/src/main/res/drawable-xhdpi/pause_default.png deleted file mode 100644 index 78841141d..000000000 Binary files a/app/src/main/res/drawable-xhdpi/pause_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/reply_message_default.png b/app/src/main/res/drawable-xhdpi/reply_message_default.png deleted file mode 100644 index f0d2ff97f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/reply_message_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/route_bluetooth_default.png b/app/src/main/res/drawable-xhdpi/route_bluetooth_default.png deleted file mode 100644 index 46793a483..000000000 Binary files a/app/src/main/res/drawable-xhdpi/route_bluetooth_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/route_earpiece_default.png b/app/src/main/res/drawable-xhdpi/route_earpiece_default.png deleted file mode 100644 index 9a4950867..000000000 Binary files a/app/src/main/res/drawable-xhdpi/route_earpiece_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/route_speaker_default.png b/app/src/main/res/drawable-xhdpi/route_speaker_default.png deleted file mode 100644 index e3621092e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/route_speaker_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/routes_default.png b/app/src/main/res/drawable-xhdpi/routes_default.png deleted file mode 100644 index cd78857df..000000000 Binary files a/app/src/main/res/drawable-xhdpi/routes_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/screenshot_default.png b/app/src/main/res/drawable-xhdpi/screenshot_default.png deleted file mode 100644 index 1808d9f3e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/screenshot_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/settings_conferences_default.png b/app/src/main/res/drawable-xhdpi/settings_conferences_default.png new file mode 100644 index 000000000..97a08dba0 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/settings_conferences_default.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_audio_routes.png b/app/src/main/res/drawable-xhdpi/voip_audio_routes.png new file mode 100644 index 000000000..fae4e84e3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_audio_routes.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_bluetooth.png b/app/src/main/res/drawable-xhdpi/voip_bluetooth.png new file mode 100644 index 000000000..3010fb418 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_bluetooth.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call.png b/app/src/main/res/drawable-xhdpi/voip_call.png new file mode 100644 index 000000000..1a95c052e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_call.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_add.png b/app/src/main/res/drawable-xhdpi/voip_call_add.png new file mode 100644 index 000000000..8237f3305 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_call_add.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_chat.png b/app/src/main/res/drawable-xhdpi/voip_call_chat.png new file mode 100644 index 000000000..be4a138de Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_call_chat.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_forward.png b/app/src/main/res/drawable-xhdpi/voip_call_forward.png new file mode 100644 index 000000000..cc61de482 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_call_forward.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_header_active.png b/app/src/main/res/drawable-xhdpi/voip_call_header_active.png new file mode 100644 index 000000000..3ef0106bc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_call_header_active.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_header_incoming.png b/app/src/main/res/drawable-xhdpi/voip_call_header_incoming.png new file mode 100644 index 000000000..7a8d458ad Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_call_header_incoming.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_header_outgoing.png b/app/src/main/res/drawable-xhdpi/voip_call_header_outgoing.png new file mode 100644 index 000000000..474abe754 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_call_header_outgoing.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_header_paused.png b/app/src/main/res/drawable-xhdpi/voip_call_header_paused.png new file mode 100644 index 000000000..fdcfaf5d2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_call_header_paused.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_list_menu.png b/app/src/main/res/drawable-xhdpi/voip_call_list_menu.png new file mode 100644 index 000000000..fdd82bea6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_call_list_menu.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_more.png b/app/src/main/res/drawable-xhdpi/voip_call_more.png new file mode 100644 index 000000000..73bb13b36 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_call_more.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_numpad.png b/app/src/main/res/drawable-xhdpi/voip_call_numpad.png new file mode 100644 index 000000000..f6e562387 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_call_numpad.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_participants.png b/app/src/main/res/drawable-xhdpi/voip_call_participants.png new file mode 100644 index 000000000..b2b766b14 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_call_participants.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_record.png b/app/src/main/res/drawable-xhdpi/voip_call_record.png new file mode 100644 index 000000000..9c392dc47 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_call_record.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_stats.png b/app/src/main/res/drawable-xhdpi/voip_call_stats.png new file mode 100644 index 000000000..018f605f6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_call_stats.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_calls_list.png b/app/src/main/res/drawable-xhdpi/voip_calls_list.png new file mode 100644 index 000000000..a3d69b84e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_calls_list.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_camera_off.png b/app/src/main/res/drawable-xhdpi/voip_camera_off.png new file mode 100644 index 000000000..dd41c338b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_camera_off.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_camera_on.png b/app/src/main/res/drawable-xhdpi/voip_camera_on.png new file mode 100644 index 000000000..7109c577d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_camera_on.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_cancel.png b/app/src/main/res/drawable-xhdpi/voip_cancel.png new file mode 100644 index 000000000..493b35e79 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_cancel.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_change_camera.png b/app/src/main/res/drawable-xhdpi/voip_change_camera.png new file mode 100644 index 000000000..d6dc15cb8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_change_camera.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_chat_rooms_list.png b/app/src/main/res/drawable-xhdpi/voip_chat_rooms_list.png new file mode 100644 index 000000000..edf722c7e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_chat_rooms_list.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_conference_active_speaker.png b/app/src/main/res/drawable-xhdpi/voip_conference_active_speaker.png new file mode 100644 index 000000000..18f21106a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_conference_active_speaker.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_conference_mosaic.png b/app/src/main/res/drawable-xhdpi/voip_conference_mosaic.png new file mode 100644 index 000000000..8fa0137b7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_conference_mosaic.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_conference_new.png b/app/src/main/res/drawable-xhdpi/voip_conference_new.png new file mode 100644 index 000000000..8985782ae Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_conference_new.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_conference_paused_big.png b/app/src/main/res/drawable-xhdpi/voip_conference_paused_big.png new file mode 100644 index 000000000..745f17220 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_conference_paused_big.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_conference_play_big.png b/app/src/main/res/drawable-xhdpi/voip_conference_play_big.png new file mode 100644 index 000000000..303d05faa Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_conference_play_big.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_copy.png b/app/src/main/res/drawable-xhdpi/voip_copy.png new file mode 100644 index 000000000..43639693e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_copy.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_delete.png b/app/src/main/res/drawable-xhdpi/voip_delete.png new file mode 100644 index 000000000..3022d156d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_delete.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_dropdown.png b/app/src/main/res/drawable-xhdpi/voip_dropdown.png new file mode 100644 index 000000000..d9fccac91 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_dropdown.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_earpiece.png b/app/src/main/res/drawable-xhdpi/voip_earpiece.png new file mode 100644 index 000000000..e95a30801 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_earpiece.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_edit.png b/app/src/main/res/drawable-xhdpi/voip_edit.png new file mode 100644 index 000000000..c24930212 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_edit.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_export.png b/app/src/main/res/drawable-xhdpi/voip_export.png new file mode 100644 index 000000000..3fdfa078a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_export.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_hangup.png b/app/src/main/res/drawable-xhdpi/voip_hangup.png new file mode 100644 index 000000000..a2ceab5d8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_hangup.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_info.png b/app/src/main/res/drawable-xhdpi/voip_info.png new file mode 100644 index 000000000..ae8dd86f8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_info.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_mandatory.png b/app/src/main/res/drawable-xhdpi/voip_mandatory.png new file mode 100644 index 000000000..4be37f7e4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_mandatory.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_menu_more.png b/app/src/main/res/drawable-xhdpi/voip_menu_more.png new file mode 100644 index 000000000..7f7f7d8fd Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_menu_more.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_merge_calls.png b/app/src/main/res/drawable-xhdpi/voip_merge_calls.png new file mode 100644 index 000000000..6c4da5988 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_merge_calls.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_micro_off.png b/app/src/main/res/drawable-xhdpi/voip_micro_off.png new file mode 100644 index 000000000..57569b4f2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_micro_off.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_micro_on.png b/app/src/main/res/drawable-xhdpi/voip_micro_on.png new file mode 100644 index 000000000..6552d35d5 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_micro_on.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_multiple_contacts_avatar.png b/app/src/main/res/drawable-xhdpi/voip_multiple_contacts_avatar.png new file mode 100644 index 000000000..78b10f11b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_multiple_contacts_avatar.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_0.png b/app/src/main/res/drawable-xhdpi/voip_numpad_0.png new file mode 100644 index 000000000..115bdb17d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_numpad_0.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_1.png b/app/src/main/res/drawable-xhdpi/voip_numpad_1.png new file mode 100644 index 000000000..4d8b7f5cc Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_numpad_1.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_2.png b/app/src/main/res/drawable-xhdpi/voip_numpad_2.png new file mode 100644 index 000000000..6b561c468 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_numpad_2.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_3.png b/app/src/main/res/drawable-xhdpi/voip_numpad_3.png new file mode 100644 index 000000000..386715586 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_numpad_3.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_4.png b/app/src/main/res/drawable-xhdpi/voip_numpad_4.png new file mode 100644 index 000000000..e3dfdcc51 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_numpad_4.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_5.png b/app/src/main/res/drawable-xhdpi/voip_numpad_5.png new file mode 100644 index 000000000..a18af28e5 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_numpad_5.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_6.png b/app/src/main/res/drawable-xhdpi/voip_numpad_6.png new file mode 100644 index 000000000..79279cb99 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_numpad_6.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_7.png b/app/src/main/res/drawable-xhdpi/voip_numpad_7.png new file mode 100644 index 000000000..c68656fd3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_numpad_7.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_8.png b/app/src/main/res/drawable-xhdpi/voip_numpad_8.png new file mode 100644 index 000000000..8d84c96ba Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_numpad_8.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_9.png b/app/src/main/res/drawable-xhdpi/voip_numpad_9.png new file mode 100644 index 000000000..af3e0e0bf Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_numpad_9.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_hash.png b/app/src/main/res/drawable-xhdpi/voip_numpad_hash.png new file mode 100644 index 000000000..790e7d12e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_numpad_hash.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_star.png b/app/src/main/res/drawable-xhdpi/voip_numpad_star.png new file mode 100644 index 000000000..5a2649de4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_numpad_star.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_pause.png b/app/src/main/res/drawable-xhdpi/voip_pause.png new file mode 100644 index 000000000..e888da937 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_pause.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_remote_recording.png b/app/src/main/res/drawable-xhdpi/voip_remote_recording.png new file mode 100644 index 000000000..3e4e94009 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_remote_recording.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_single_contact_avatar.png b/app/src/main/res/drawable-xhdpi/voip_single_contact_avatar.png new file mode 100644 index 000000000..5d158fa49 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_single_contact_avatar.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_speaker_off.png b/app/src/main/res/drawable-xhdpi/voip_speaker_off.png new file mode 100644 index 000000000..c018c9899 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_speaker_off.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_speaker_on.png b/app/src/main/res/drawable-xhdpi/voip_speaker_on.png new file mode 100644 index 000000000..07f13c9b6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_speaker_on.png differ diff --git a/app/src/main/res/drawable-xhdpi/voip_spinner.png b/app/src/main/res/drawable-xhdpi/voip_spinner.png new file mode 100644 index 000000000..8238de56b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/voip_spinner.png differ diff --git a/app/src/main/res/drawable-xhdpi/waiting_time.png b/app/src/main/res/drawable-xhdpi/waiting_time.png deleted file mode 100644 index cd949a26f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/waiting_time.png and /dev/null differ diff --git a/app/src/main/res/drawable/button_background.xml b/app/src/main/res/drawable/button_background.xml new file mode 100644 index 000000000..1b672fdfc --- /dev/null +++ b/app/src/main/res/drawable/button_background.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/button_background_dark.xml b/app/src/main/res/drawable/button_background_dark.xml index 034a4fd2e..8f13c7bd7 100644 --- a/app/src/main/res/drawable/button_background_dark.xml +++ b/app/src/main/res/drawable/button_background_dark.xml @@ -3,9 +3,9 @@ + android:drawable="@color/voip_dark_color" /> + android:drawable="@color/voip_dark_color" /> + android:drawable="@color/voip_dark_color" /> diff --git a/app/src/main/res/drawable/button_background_reverse.xml b/app/src/main/res/drawable/button_background_reverse.xml new file mode 100644 index 000000000..9b7432f25 --- /dev/null +++ b/app/src/main/res/drawable/button_background_reverse.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/button_call_answer_background.xml b/app/src/main/res/drawable/button_call_answer_background.xml new file mode 100644 index 000000000..e7f8c8abd --- /dev/null +++ b/app/src/main/res/drawable/button_call_answer_background.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable/button_call_context_menu_background.xml b/app/src/main/res/drawable/button_call_context_menu_background.xml new file mode 100644 index 000000000..c20f7aa56 --- /dev/null +++ b/app/src/main/res/drawable/button_call_context_menu_background.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable/button_call_numpad_background.xml b/app/src/main/res/drawable/button_call_numpad_background.xml new file mode 100644 index 000000000..b30990ebf --- /dev/null +++ b/app/src/main/res/drawable/button_call_numpad_background.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable/button_call_recording_background.xml b/app/src/main/res/drawable/button_call_recording_background.xml new file mode 100644 index 000000000..1f3680b7c --- /dev/null +++ b/app/src/main/res/drawable/button_call_recording_background.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/button_call_terminate_background.xml b/app/src/main/res/drawable/button_call_terminate_background.xml new file mode 100644 index 000000000..f584585fe --- /dev/null +++ b/app/src/main/res/drawable/button_call_terminate_background.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable/button_conference_info.xml b/app/src/main/res/drawable/button_conference_info.xml new file mode 100644 index 000000000..89ec60ccc --- /dev/null +++ b/app/src/main/res/drawable/button_conference_info.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/button_green_background.xml b/app/src/main/res/drawable/button_green_background.xml new file mode 100644 index 000000000..1ebc96355 --- /dev/null +++ b/app/src/main/res/drawable/button_green_background.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/app/src/main/res/drawable/button_round_background.xml b/app/src/main/res/drawable/button_round_background.xml new file mode 100644 index 000000000..afc56ca3b --- /dev/null +++ b/app/src/main/res/drawable/button_round_background.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/button_toggle_background.xml b/app/src/main/res/drawable/button_toggle_background.xml new file mode 100644 index 000000000..c2292dae4 --- /dev/null +++ b/app/src/main/res/drawable/button_toggle_background.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/src/main/res/drawable/button_toggle_background_reverse.xml b/app/src/main/res/drawable/button_toggle_background_reverse.xml new file mode 100644 index 000000000..3ef75d708 --- /dev/null +++ b/app/src/main/res/drawable/button_toggle_background_reverse.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/src/main/res/drawable/call_hangup_background.xml b/app/src/main/res/drawable/call_hangup_background.xml deleted file mode 100644 index 2e55eaf93..000000000 --- a/app/src/main/res/drawable/call_hangup_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/call_numpad.xml b/app/src/main/res/drawable/call_numpad.xml deleted file mode 100644 index 340e506d4..000000000 --- a/app/src/main/res/drawable/call_numpad.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/drawable/call_pause.xml b/app/src/main/res/drawable/call_pause.xml deleted file mode 100644 index 6cc693d86..000000000 --- a/app/src/main/res/drawable/call_pause.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/call_screenshot.xml b/app/src/main/res/drawable/call_screenshot.xml deleted file mode 100644 index 638610ae3..000000000 --- a/app/src/main/res/drawable/call_screenshot.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/camera.xml b/app/src/main/res/drawable/camera.xml deleted file mode 100644 index d30ef1407..000000000 --- a/app/src/main/res/drawable/camera.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/chat_room_menu_security.xml b/app/src/main/res/drawable/chat_room_menu_devices.xml similarity index 100% rename from app/src/main/res/drawable/chat_room_menu_security.xml rename to app/src/main/res/drawable/chat_room_menu_devices.xml diff --git a/app/src/main/res/drawable/checkbox.xml b/app/src/main/res/drawable/checkbox.xml deleted file mode 100644 index accbf4d23..000000000 --- a/app/src/main/res/drawable/checkbox.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/download.xml b/app/src/main/res/drawable/download.xml deleted file mode 100644 index 1304c34b0..000000000 --- a/app/src/main/res/drawable/download.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable/field_button_background.xml b/app/src/main/res/drawable/field_button_background.xml deleted file mode 100644 index 6e7b32df8..000000000 --- a/app/src/main/res/drawable/field_button_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/footer_button.xml b/app/src/main/res/drawable/footer_button.xml index e0e1f6cbb..b23178869 100644 --- a/app/src/main/res/drawable/footer_button.xml +++ b/app/src/main/res/drawable/footer_button.xml @@ -3,7 +3,7 @@ + android:drawable="@color/voip_dark_color" /> + android:drawable="@color/voip_dark_color" /> diff --git a/app/src/main/res/drawable/menu_more.xml b/app/src/main/res/drawable/icon_apply.xml similarity index 70% rename from app/src/main/res/drawable/menu_more.xml rename to app/src/main/res/drawable/icon_apply.xml index 8e9603b3f..3be5be72e 100644 --- a/app/src/main/res/drawable/menu_more.xml +++ b/app/src/main/res/drawable/icon_apply.xml @@ -1,15 +1,15 @@ - - - diff --git a/app/src/main/res/drawable/record_stop_dark.xml b/app/src/main/res/drawable/icon_audio_routes.xml similarity index 56% rename from app/src/main/res/drawable/record_stop_dark.xml rename to app/src/main/res/drawable/icon_audio_routes.xml index c6d3ef924..b694e1b51 100644 --- a/app/src/main/res/drawable/record_stop_dark.xml +++ b/app/src/main/res/drawable/icon_audio_routes.xml @@ -1,8 +1,7 @@ - + - diff --git a/app/src/main/res/drawable/icon_bluetooth.xml b/app/src/main/res/drawable/icon_bluetooth.xml new file mode 100644 index 000000000..42ad75065 --- /dev/null +++ b/app/src/main/res/drawable/icon_bluetooth.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_call_add.xml b/app/src/main/res/drawable/icon_call_add.xml new file mode 100644 index 000000000..be2d29f8d --- /dev/null +++ b/app/src/main/res/drawable/icon_call_add.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_call_answer.xml b/app/src/main/res/drawable/icon_call_answer.xml new file mode 100644 index 000000000..86a06a63b --- /dev/null +++ b/app/src/main/res/drawable/icon_call_answer.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_call_camera_switch.xml b/app/src/main/res/drawable/icon_call_camera_switch.xml new file mode 100644 index 000000000..d8b0d6de6 --- /dev/null +++ b/app/src/main/res/drawable/icon_call_camera_switch.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_call_chat.xml b/app/src/main/res/drawable/icon_call_chat.xml new file mode 100644 index 000000000..a7fa4fa36 --- /dev/null +++ b/app/src/main/res/drawable/icon_call_chat.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/icon_call_chat_rooms.xml b/app/src/main/res/drawable/icon_call_chat_rooms.xml new file mode 100644 index 000000000..584556375 --- /dev/null +++ b/app/src/main/res/drawable/icon_call_chat_rooms.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_call_forward.xml b/app/src/main/res/drawable/icon_call_forward.xml new file mode 100644 index 000000000..f8b6134ed --- /dev/null +++ b/app/src/main/res/drawable/icon_call_forward.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_call_hangup.xml b/app/src/main/res/drawable/icon_call_hangup.xml new file mode 100644 index 000000000..35cc67534 --- /dev/null +++ b/app/src/main/res/drawable/icon_call_hangup.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_call_more.xml b/app/src/main/res/drawable/icon_call_more.xml new file mode 100644 index 000000000..488e9e3bf --- /dev/null +++ b/app/src/main/res/drawable/icon_call_more.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_call_numpad.xml b/app/src/main/res/drawable/icon_call_numpad.xml new file mode 100644 index 000000000..56932679b --- /dev/null +++ b/app/src/main/res/drawable/icon_call_numpad.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/icon_call_participants.xml b/app/src/main/res/drawable/icon_call_participants.xml new file mode 100644 index 000000000..a36b93b60 --- /dev/null +++ b/app/src/main/res/drawable/icon_call_participants.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/icon_call_record.xml b/app/src/main/res/drawable/icon_call_record.xml new file mode 100644 index 000000000..7051c16aa --- /dev/null +++ b/app/src/main/res/drawable/icon_call_record.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/icon_call_stats.xml b/app/src/main/res/drawable/icon_call_stats.xml new file mode 100644 index 000000000..d506adbbf --- /dev/null +++ b/app/src/main/res/drawable/icon_call_stats.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/icon_calls_list.xml b/app/src/main/res/drawable/icon_calls_list.xml new file mode 100644 index 000000000..2c85e187a --- /dev/null +++ b/app/src/main/res/drawable/icon_calls_list.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/icon_cancel.xml b/app/src/main/res/drawable/icon_cancel.xml new file mode 100644 index 000000000..052b79e93 --- /dev/null +++ b/app/src/main/res/drawable/icon_cancel.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_cancel_alt.xml b/app/src/main/res/drawable/icon_cancel_alt.xml new file mode 100644 index 000000000..07d6cc058 --- /dev/null +++ b/app/src/main/res/drawable/icon_cancel_alt.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_conf_layout.xml b/app/src/main/res/drawable/icon_conf_layout.xml new file mode 100644 index 000000000..f5d2aea2d --- /dev/null +++ b/app/src/main/res/drawable/icon_conf_layout.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/icon_conference_layout_active_speaker.xml b/app/src/main/res/drawable/icon_conference_layout_active_speaker.xml new file mode 100644 index 000000000..cb565b345 --- /dev/null +++ b/app/src/main/res/drawable/icon_conference_layout_active_speaker.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_conference_layout_grid.xml b/app/src/main/res/drawable/icon_conference_layout_grid.xml new file mode 100644 index 000000000..8a66fb552 --- /dev/null +++ b/app/src/main/res/drawable/icon_conference_layout_grid.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_copy.xml b/app/src/main/res/drawable/icon_copy.xml new file mode 100644 index 000000000..278fa0562 --- /dev/null +++ b/app/src/main/res/drawable/icon_copy.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_delete.xml b/app/src/main/res/drawable/icon_delete.xml new file mode 100644 index 000000000..bd647fa54 --- /dev/null +++ b/app/src/main/res/drawable/icon_delete.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_earpiece.xml b/app/src/main/res/drawable/icon_earpiece.xml new file mode 100644 index 000000000..69b03ecc0 --- /dev/null +++ b/app/src/main/res/drawable/icon_earpiece.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_edit.xml b/app/src/main/res/drawable/icon_edit.xml new file mode 100644 index 000000000..d9a3f921a --- /dev/null +++ b/app/src/main/res/drawable/icon_edit.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_edit_alt.xml b/app/src/main/res/drawable/icon_edit_alt.xml new file mode 100644 index 000000000..74da8157b --- /dev/null +++ b/app/src/main/res/drawable/icon_edit_alt.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_info.xml b/app/src/main/res/drawable/icon_info.xml new file mode 100644 index 000000000..99fd6a38f --- /dev/null +++ b/app/src/main/res/drawable/icon_info.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_info_selected.xml b/app/src/main/res/drawable/icon_info_selected.xml new file mode 100644 index 000000000..67b628ef2 --- /dev/null +++ b/app/src/main/res/drawable/icon_info_selected.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/options_add_call.xml b/app/src/main/res/drawable/icon_menu_more.xml similarity index 68% rename from app/src/main/res/drawable/options_add_call.xml rename to app/src/main/res/drawable/icon_menu_more.xml index 97922cc63..19c7e24b7 100644 --- a/app/src/main/res/drawable/options_add_call.xml +++ b/app/src/main/res/drawable/icon_menu_more.xml @@ -1,15 +1,15 @@ - - - diff --git a/app/src/main/res/drawable/icon_merge_calls_local_conference.xml b/app/src/main/res/drawable/icon_merge_calls_local_conference.xml new file mode 100644 index 000000000..2fdd2e68a --- /dev/null +++ b/app/src/main/res/drawable/icon_merge_calls_local_conference.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/options_transfer_call.xml b/app/src/main/res/drawable/icon_next.xml similarity index 66% rename from app/src/main/res/drawable/options_transfer_call.xml rename to app/src/main/res/drawable/icon_next.xml index d979d34db..968192312 100644 --- a/app/src/main/res/drawable/options_transfer_call.xml +++ b/app/src/main/res/drawable/icon_next.xml @@ -1,15 +1,15 @@ - - - diff --git a/app/src/main/res/drawable/icon_pause.xml b/app/src/main/res/drawable/icon_pause.xml new file mode 100644 index 000000000..09787beba --- /dev/null +++ b/app/src/main/res/drawable/icon_pause.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_schedule_date.xml b/app/src/main/res/drawable/icon_schedule_date.xml new file mode 100644 index 000000000..5f29ac622 --- /dev/null +++ b/app/src/main/res/drawable/icon_schedule_date.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_schedule_participants.xml b/app/src/main/res/drawable/icon_schedule_participants.xml new file mode 100644 index 000000000..45741792a --- /dev/null +++ b/app/src/main/res/drawable/icon_schedule_participants.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/conference.xml b/app/src/main/res/drawable/icon_schedule_time.xml similarity index 56% rename from app/src/main/res/drawable/conference.xml rename to app/src/main/res/drawable/icon_schedule_time.xml index 25f309f2f..f165174c6 100644 --- a/app/src/main/res/drawable/conference.xml +++ b/app/src/main/res/drawable/icon_schedule_time.xml @@ -1,7 +1,7 @@ - + diff --git a/app/src/main/res/drawable/conference_remove_participant.xml b/app/src/main/res/drawable/icon_share.xml similarity index 75% rename from app/src/main/res/drawable/conference_remove_participant.xml rename to app/src/main/res/drawable/icon_share.xml index d8fcc664b..4b7a252ee 100644 --- a/app/src/main/res/drawable/conference_remove_participant.xml +++ b/app/src/main/res/drawable/icon_share.xml @@ -1,8 +1,9 @@ - + diff --git a/app/src/main/res/drawable/icon_speaker.xml b/app/src/main/res/drawable/icon_speaker.xml new file mode 100644 index 000000000..ab3fd031b --- /dev/null +++ b/app/src/main/res/drawable/icon_speaker.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_spinner.xml b/app/src/main/res/drawable/icon_spinner.xml new file mode 100644 index 000000000..f3537e7bd --- /dev/null +++ b/app/src/main/res/drawable/icon_spinner.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/icon_spinner_background.xml b/app/src/main/res/drawable/icon_spinner_background.xml new file mode 100644 index 000000000..de81ede9d --- /dev/null +++ b/app/src/main/res/drawable/icon_spinner_background.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/icon_spinner_rotating.xml b/app/src/main/res/drawable/icon_spinner_rotating.xml new file mode 100644 index 000000000..6b8ed48ca --- /dev/null +++ b/app/src/main/res/drawable/icon_spinner_rotating.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/drawable/icon_toggle_camera.xml b/app/src/main/res/drawable/icon_toggle_camera.xml new file mode 100644 index 000000000..80a10a84f --- /dev/null +++ b/app/src/main/res/drawable/icon_toggle_camera.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/icon_toggle_mic.xml b/app/src/main/res/drawable/icon_toggle_mic.xml new file mode 100644 index 000000000..40b18e3f8 --- /dev/null +++ b/app/src/main/res/drawable/icon_toggle_mic.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/icon_toggle_speaker.xml b/app/src/main/res/drawable/icon_toggle_speaker.xml new file mode 100644 index 000000000..e9f43e1a1 --- /dev/null +++ b/app/src/main/res/drawable/icon_toggle_speaker.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/icon_video_conf_new.xml b/app/src/main/res/drawable/icon_video_conf_new.xml new file mode 100644 index 000000000..ec949655b --- /dev/null +++ b/app/src/main/res/drawable/icon_video_conf_new.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/drawable/micro.xml b/app/src/main/res/drawable/micro.xml deleted file mode 100644 index 888275bc4..000000000 --- a/app/src/main/res/drawable/micro.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/options.xml b/app/src/main/res/drawable/options.xml deleted file mode 100644 index 4b8c7d20e..000000000 --- a/app/src/main/res/drawable/options.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/options_rec.xml b/app/src/main/res/drawable/options_rec.xml deleted file mode 100644 index 3d75c109a..000000000 --- a/app/src/main/res/drawable/options_rec.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/options_start_conference.xml b/app/src/main/res/drawable/options_start_conference.xml deleted file mode 100644 index 1a974f2e9..000000000 --- a/app/src/main/res/drawable/options_start_conference.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/route_bluetooth.xml b/app/src/main/res/drawable/route_bluetooth.xml deleted file mode 100644 index 072a96f2c..000000000 --- a/app/src/main/res/drawable/route_bluetooth.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/route_earpiece.xml b/app/src/main/res/drawable/route_earpiece.xml deleted file mode 100644 index 3549a4b20..000000000 --- a/app/src/main/res/drawable/route_earpiece.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/route_speaker.xml b/app/src/main/res/drawable/route_speaker.xml deleted file mode 100644 index 6d4d54da5..000000000 --- a/app/src/main/res/drawable/route_speaker.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/routes.xml b/app/src/main/res/drawable/routes.xml deleted file mode 100644 index 3d0fd6f0a..000000000 --- a/app/src/main/res/drawable/routes.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/security_switch_track.xml b/app/src/main/res/drawable/security_switch_track.xml deleted file mode 100644 index 943b363d1..000000000 --- a/app/src/main/res/drawable/security_switch_track.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/settings_conferences.xml b/app/src/main/res/drawable/settings_conferences.xml new file mode 100644 index 000000000..1c4d4d873 --- /dev/null +++ b/app/src/main/res/drawable/settings_conferences.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/shape_audio_routes_background.xml b/app/src/main/res/drawable/shape_audio_routes_background.xml new file mode 100644 index 000000000..25038835b --- /dev/null +++ b/app/src/main/res/drawable/shape_audio_routes_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_button_background.xml b/app/src/main/res/drawable/shape_button_background.xml new file mode 100644 index 000000000..b12bba2b6 --- /dev/null +++ b/app/src/main/res/drawable/shape_button_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_button_disabled_background.xml b/app/src/main/res/drawable/shape_button_disabled_background.xml new file mode 100644 index 000000000..b9a64291c --- /dev/null +++ b/app/src/main/res/drawable/shape_button_disabled_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_button_pressed_background.xml b/app/src/main/res/drawable/shape_button_pressed_background.xml new file mode 100644 index 000000000..adcc6ce8b --- /dev/null +++ b/app/src/main/res/drawable/shape_button_pressed_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_call_answer_background.xml b/app/src/main/res/drawable/shape_call_answer_background.xml new file mode 100644 index 000000000..f456f09ef --- /dev/null +++ b/app/src/main/res/drawable/shape_call_answer_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_call_answer_pressed_background.xml b/app/src/main/res/drawable/shape_call_answer_pressed_background.xml new file mode 100644 index 000000000..72b56a9a5 --- /dev/null +++ b/app/src/main/res/drawable/shape_call_answer_pressed_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/field_button_background_default.xml b/app/src/main/res/drawable/shape_call_contact_avatar_background.xml similarity index 55% rename from app/src/main/res/drawable/field_button_background_default.xml rename to app/src/main/res/drawable/shape_call_contact_avatar_background.xml index 579aad9ab..a042ac939 100644 --- a/app/src/main/res/drawable/field_button_background_default.xml +++ b/app/src/main/res/drawable/shape_call_contact_avatar_background.xml @@ -1,5 +1,5 @@ - - + + diff --git a/app/src/main/res/drawable/field_button_background_over.xml b/app/src/main/res/drawable/shape_call_contact_avatar_background_alt.xml similarity index 52% rename from app/src/main/res/drawable/field_button_background_over.xml rename to app/src/main/res/drawable/shape_call_contact_avatar_background_alt.xml index 6be50af16..397dd51bb 100644 --- a/app/src/main/res/drawable/field_button_background_over.xml +++ b/app/src/main/res/drawable/shape_call_contact_avatar_background_alt.xml @@ -1,5 +1,5 @@ - - + + diff --git a/app/src/main/res/drawable/shape_call_numpad_background.xml b/app/src/main/res/drawable/shape_call_numpad_background.xml new file mode 100644 index 000000000..d5a2c3cd7 --- /dev/null +++ b/app/src/main/res/drawable/shape_call_numpad_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_call_numpad_pressed_background.xml b/app/src/main/res/drawable/shape_call_numpad_pressed_background.xml new file mode 100644 index 000000000..8e2fd88e2 --- /dev/null +++ b/app/src/main/res/drawable/shape_call_numpad_pressed_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_call_popup_background.xml b/app/src/main/res/drawable/shape_call_popup_background.xml new file mode 100644 index 000000000..9c9303055 --- /dev/null +++ b/app/src/main/res/drawable/shape_call_popup_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_call_recording_off_background.xml b/app/src/main/res/drawable/shape_call_recording_off_background.xml new file mode 100644 index 000000000..8fb61094a --- /dev/null +++ b/app/src/main/res/drawable/shape_call_recording_off_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_call_recording_on_background.xml b/app/src/main/res/drawable/shape_call_recording_on_background.xml new file mode 100644 index 000000000..35da9f606 --- /dev/null +++ b/app/src/main/res/drawable/shape_call_recording_on_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_call_terminate_background.xml b/app/src/main/res/drawable/shape_call_terminate_background.xml new file mode 100644 index 000000000..a8b903a58 --- /dev/null +++ b/app/src/main/res/drawable/shape_call_terminate_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_call_terminate_pressed_background.xml b/app/src/main/res/drawable/shape_call_terminate_pressed_background.xml new file mode 100644 index 000000000..622eae85f --- /dev/null +++ b/app/src/main/res/drawable/shape_call_terminate_pressed_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_conference_active_speaker_border.xml b/app/src/main/res/drawable/shape_conference_active_speaker_border.xml new file mode 100644 index 000000000..98a0c1b7f --- /dev/null +++ b/app/src/main/res/drawable/shape_conference_active_speaker_border.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_conference_divider.xml b/app/src/main/res/drawable/shape_conference_divider.xml new file mode 100644 index 000000000..609aae7d2 --- /dev/null +++ b/app/src/main/res/drawable/shape_conference_divider.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_conference_divider_fullscreen.xml b/app/src/main/res/drawable/shape_conference_divider_fullscreen.xml new file mode 100644 index 000000000..105fefea9 --- /dev/null +++ b/app/src/main/res/drawable/shape_conference_divider_fullscreen.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_conference_invite_background.xml b/app/src/main/res/drawable/shape_conference_invite_background.xml new file mode 100644 index 000000000..041c8baa4 --- /dev/null +++ b/app/src/main/res/drawable/shape_conference_invite_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_conference_selected_border.xml b/app/src/main/res/drawable/shape_conference_selected_border.xml new file mode 100644 index 000000000..8bc5cd57f --- /dev/null +++ b/app/src/main/res/drawable/shape_conference_selected_border.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_dialog_background.xml b/app/src/main/res/drawable/shape_dialog_background.xml new file mode 100644 index 000000000..1321d5ec0 --- /dev/null +++ b/app/src/main/res/drawable/shape_dialog_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_edittext_background.xml b/app/src/main/res/drawable/shape_edittext_background.xml new file mode 100644 index 000000000..7738c0b77 --- /dev/null +++ b/app/src/main/res/drawable/shape_edittext_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_extra_buttons_background.xml b/app/src/main/res/drawable/shape_extra_buttons_background.xml new file mode 100644 index 000000000..294ee7cb4 --- /dev/null +++ b/app/src/main/res/drawable/shape_extra_buttons_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_form_field_background.xml b/app/src/main/res/drawable/shape_form_field_background.xml new file mode 100644 index 000000000..fce78c8c5 --- /dev/null +++ b/app/src/main/res/drawable/shape_form_field_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_orange_circle.xml b/app/src/main/res/drawable/shape_orange_circle.xml new file mode 100644 index 000000000..d894e038a --- /dev/null +++ b/app/src/main/res/drawable/shape_orange_circle.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/security_switch_thumb.xml b/app/src/main/res/drawable/shape_rect_gray_button.xml similarity index 56% rename from app/src/main/res/drawable/security_switch_thumb.xml rename to app/src/main/res/drawable/shape_rect_gray_button.xml index f5898deab..7bd9f74ca 100644 --- a/app/src/main/res/drawable/security_switch_thumb.xml +++ b/app/src/main/res/drawable/shape_rect_gray_button.xml @@ -1,6 +1,6 @@ - + + - + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_rect_green_button.xml b/app/src/main/res/drawable/shape_rect_green_button.xml new file mode 100644 index 000000000..0a51a8db5 --- /dev/null +++ b/app/src/main/res/drawable/shape_rect_green_button.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_rect_orange_button.xml b/app/src/main/res/drawable/shape_rect_orange_button.xml new file mode 100644 index 000000000..e239fdbfc --- /dev/null +++ b/app/src/main/res/drawable/shape_rect_orange_button.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_remote_background.xml b/app/src/main/res/drawable/shape_remote_background.xml new file mode 100644 index 000000000..f77318418 --- /dev/null +++ b/app/src/main/res/drawable/shape_remote_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_remote_paused_background.xml b/app/src/main/res/drawable/shape_remote_paused_background.xml new file mode 100644 index 000000000..8663b555c --- /dev/null +++ b/app/src/main/res/drawable/shape_remote_paused_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_remote_recording_background.xml b/app/src/main/res/drawable/shape_remote_recording_background.xml new file mode 100644 index 000000000..7247f620a --- /dev/null +++ b/app/src/main/res/drawable/shape_remote_recording_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/call_video_conference_participant_background.xml b/app/src/main/res/drawable/shape_remote_video_background.xml similarity index 79% rename from app/src/main/res/drawable/call_video_conference_participant_background.xml rename to app/src/main/res/drawable/shape_remote_video_background.xml index f3cf29b28..6b924bcef 100644 --- a/app/src/main/res/drawable/call_video_conference_participant_background.xml +++ b/app/src/main/res/drawable/shape_remote_video_background.xml @@ -1,5 +1,5 @@ + - - + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_round_button.xml b/app/src/main/res/drawable/shape_round_button.xml new file mode 100644 index 000000000..e4607cb25 --- /dev/null +++ b/app/src/main/res/drawable/shape_round_button.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_round_button_disabled.xml b/app/src/main/res/drawable/shape_round_button_disabled.xml new file mode 100644 index 000000000..b9a64291c --- /dev/null +++ b/app/src/main/res/drawable/shape_round_button_disabled.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_round_button_pressed.xml b/app/src/main/res/drawable/shape_round_button_pressed.xml new file mode 100644 index 000000000..adcc6ce8b --- /dev/null +++ b/app/src/main/res/drawable/shape_round_button_pressed.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_round_gray_background.xml b/app/src/main/res/drawable/shape_round_gray_background.xml new file mode 100644 index 000000000..adc21b355 --- /dev/null +++ b/app/src/main/res/drawable/shape_round_gray_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_round_gray_background_with_orange_border.xml b/app/src/main/res/drawable/shape_round_gray_background_with_orange_border.xml new file mode 100644 index 000000000..2dd378d80 --- /dev/null +++ b/app/src/main/res/drawable/shape_round_gray_background_with_orange_border.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_toggle_disabled_background.xml b/app/src/main/res/drawable/shape_toggle_disabled_background.xml new file mode 100644 index 000000000..b9a64291c --- /dev/null +++ b/app/src/main/res/drawable/shape_toggle_disabled_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_toggle_off_background.xml b/app/src/main/res/drawable/shape_toggle_off_background.xml new file mode 100644 index 000000000..b12bba2b6 --- /dev/null +++ b/app/src/main/res/drawable/shape_toggle_off_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_toggle_on_background.xml b/app/src/main/res/drawable/shape_toggle_on_background.xml new file mode 100644 index 000000000..e4607cb25 --- /dev/null +++ b/app/src/main/res/drawable/shape_toggle_on_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_toggle_pressed_background.xml b/app/src/main/res/drawable/shape_toggle_pressed_background.xml new file mode 100644 index 000000000..adcc6ce8b --- /dev/null +++ b/app/src/main/res/drawable/shape_toggle_pressed_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/speaker.xml b/app/src/main/res/drawable/speaker.xml deleted file mode 100644 index b27e3b04b..000000000 --- a/app/src/main/res/drawable/speaker.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout-land/about_fragment.xml b/app/src/main/res/layout-land/about_fragment.xml index b57607329..9862830be 100644 --- a/app/src/main/res/layout-land/about_fragment.xml +++ b/app/src/main/res/layout-land/about_fragment.xml @@ -74,7 +74,7 @@ android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" - android:background="@color/dark_grey_color" + android:background="@color/voip_dark_color2" android:gravity="center" android:orientation="vertical" android:padding="20dp"> diff --git a/app/src/main/res/layout-land/call_controls_fragment.xml b/app/src/main/res/layout-land/call_controls_fragment.xml deleted file mode 100644 index 2e955b8b8..000000000 --- a/app/src/main/res/layout-land/call_controls_fragment.xml +++ /dev/null @@ -1,211 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/call_statistics_cell.xml b/app/src/main/res/layout-land/call_statistics_cell.xml deleted file mode 100644 index daf66caac..000000000 --- a/app/src/main/res/layout-land/call_statistics_cell.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/dialer_fragment.xml b/app/src/main/res/layout-land/dialer_fragment.xml index a9cff520c..565863732 100644 --- a/app/src/main/res/layout-land/dialer_fragment.xml +++ b/app/src/main/res/layout-land/dialer_fragment.xml @@ -6,6 +6,9 @@ + @@ -65,8 +68,20 @@ android:layout_width="match_parent" android:layout_height="@dimen/main_activity_top_bar_size"> + + + android:layout_height="match_parent"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw533dp-land/dialer_fragment.xml b/app/src/main/res/layout-sw533dp-land/dialer_fragment.xml index 4eefc4ed3..ea0688ed9 100644 --- a/app/src/main/res/layout-sw533dp-land/dialer_fragment.xml +++ b/app/src/main/res/layout-sw533dp-land/dialer_fragment.xml @@ -7,6 +7,9 @@ + @@ -106,8 +109,20 @@ android:layout_width="match_parent" android:layout_height="@dimen/main_activity_top_bar_size"> + + + @@ -106,8 +109,20 @@ android:layout_width="match_parent" android:layout_height="@dimen/main_activity_top_bar_size"> + + diff --git a/app/src/main/res/layout/call_activity.xml b/app/src/main/res/layout/call_activity.xml deleted file mode 100644 index 15c6da418..000000000 --- a/app/src/main/res/layout/call_activity.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_conference.xml b/app/src/main/res/layout/call_conference.xml deleted file mode 100644 index 4a8dd1c86..000000000 --- a/app/src/main/res/layout/call_conference.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_conference_cell.xml b/app/src/main/res/layout/call_conference_cell.xml deleted file mode 100644 index f829398c4..000000000 --- a/app/src/main/res/layout/call_conference_cell.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_conference_participant.xml b/app/src/main/res/layout/call_conference_participant.xml deleted file mode 100644 index 0f8ef7c20..000000000 --- a/app/src/main/res/layout/call_conference_participant.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_controls_fragment.xml b/app/src/main/res/layout/call_controls_fragment.xml deleted file mode 100644 index aee62bcad..000000000 --- a/app/src/main/res/layout/call_controls_fragment.xml +++ /dev/null @@ -1,205 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_incoming_activity.xml b/app/src/main/res/layout/call_incoming_activity.xml deleted file mode 100644 index 664053c96..000000000 --- a/app/src/main/res/layout/call_incoming_activity.xml +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_incoming_answer_decline_buttons.xml b/app/src/main/res/layout/call_incoming_answer_decline_buttons.xml deleted file mode 100644 index 8ee8b90a5..000000000 --- a/app/src/main/res/layout/call_incoming_answer_decline_buttons.xml +++ /dev/null @@ -1,128 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_incoming_notification_heads_up.xml b/app/src/main/res/layout/call_incoming_notification_heads_up.xml index b5b8fb38c..bb3e5907d 100644 --- a/app/src/main/res/layout/call_incoming_notification_heads_up.xml +++ b/app/src/main/res/layout/call_incoming_notification_heads_up.xml @@ -36,7 +36,7 @@ android:layout_width="50dp" android:layout_height="50dp" android:adjustViewBounds="true" - android:src="@drawable/avatar" + android:src="@drawable/voip_single_contact_avatar" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginTop="5dp" diff --git a/app/src/main/res/layout/call_outgoing_activity.xml b/app/src/main/res/layout/call_outgoing_activity.xml deleted file mode 100644 index 7e807f974..000000000 --- a/app/src/main/res/layout/call_outgoing_activity.xml +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_paused.xml b/app/src/main/res/layout/call_paused.xml deleted file mode 100644 index 5382ede33..000000000 --- a/app/src/main/res/layout/call_paused.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_paused_cell.xml b/app/src/main/res/layout/call_paused_cell.xml deleted file mode 100644 index ff965b604..000000000 --- a/app/src/main/res/layout/call_paused_cell.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_primary_buttons.xml b/app/src/main/res/layout/call_primary_buttons.xml deleted file mode 100644 index 0e57e2b50..000000000 --- a/app/src/main/res/layout/call_primary_buttons.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_secondary_buttons.xml b/app/src/main/res/layout/call_secondary_buttons.xml deleted file mode 100644 index 74db426a6..000000000 --- a/app/src/main/res/layout/call_secondary_buttons.xml +++ /dev/null @@ -1,228 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_statistics_cell_header.xml b/app/src/main/res/layout/call_statistics_cell_header.xml deleted file mode 100644 index c9f1dca7b..000000000 --- a/app/src/main/res/layout/call_statistics_cell_header.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_statistics_fragment.xml b/app/src/main/res/layout/call_statistics_fragment.xml deleted file mode 100644 index c24873765..000000000 --- a/app/src/main/res/layout/call_statistics_fragment.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_video_fragment.xml b/app/src/main/res/layout/call_video_fragment.xml deleted file mode 100644 index abb3a14a6..000000000 --- a/app/src/main/res/layout/call_video_fragment.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/chat_message_conference_invitation_content_cell.xml b/app/src/main/res/layout/chat_message_conference_invitation_content_cell.xml new file mode 100644 index 000000000..1d820fa3b --- /dev/null +++ b/app/src/main/res/layout/chat_message_conference_invitation_content_cell.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_message_content_cell.xml b/app/src/main/res/layout/chat_message_content_cell.xml index 72fa6c4be..a969f48ba 100644 --- a/app/src/main/res/layout/chat_message_content_cell.xml +++ b/app/src/main/res/layout/chat_message_content_cell.xml @@ -59,6 +59,13 @@ app:longClickListener="@{longClickListener}" android:visibility="@{!data.downloadable && data.voiceRecording ? View.VISIBLE : View.GONE, default=gone}" /> + + - + @@ -33,6 +34,9 @@ + @@ -53,9 +57,9 @@ android:background="@drawable/menu_background" android:visibility="@{resendHidden ? View.GONE : View.VISIBLE}" android:onClick="@{resendClickListener}" - android:drawableRight="@drawable/chat_send_message" style="@style/popup_item_font" - android:text="@string/chat_message_context_menu_resend" /> + android:text="@string/chat_message_context_menu_resend" + app:drawableRightCompat="@drawable/chat_send_message" /> + android:text="@string/chat_message_context_menu_copy_text" + app:drawableRightCompat="@drawable/menu_copy_text" /> + android:text="@string/chat_message_context_menu_forward" + app:drawableRightCompat="@drawable/menu_forward" /> + android:text="@string/chat_message_context_menu_reply" + app:drawableRightCompat="@drawable/menu_reply" /> + android:text="@string/chat_message_context_menu_imdn_info" + app:drawableRightCompat="@drawable/menu_imdn_info" /> + android:text="@string/chat_message_context_menu_add_to_contacts" + app:drawableRightCompat="@drawable/menu_add_contact" /> + android:text="@string/chat_message_context_menu_delete" + app:drawableRightCompat="@drawable/menu_delete" /> diff --git a/app/src/main/res/layout/chat_message_reply_content_cell.xml b/app/src/main/res/layout/chat_message_reply_content_cell.xml index 5e4acae7a..fbcdb9c62 100644 --- a/app/src/main/res/layout/chat_message_reply_content_cell.xml +++ b/app/src/main/res/layout/chat_message_reply_content_cell.xml @@ -55,7 +55,7 @@ android:gravity="center" android:text="@{data.formattedDuration, default=`00:00`}" android:textColor="@color/light_primary_text_color" - android:drawableTop="@drawable/audio_recording_reply_preview_default"/> + app:drawableTopCompat="@drawable/audio_recording_reply_preview_default" /> + app:drawableLeftCompat="@drawable/audio_recording_reply_preview_default" /> + android:textOn=""/> + app:layout="@{@layout/contact_selected_cell}" /> diff --git a/app/src/main/res/layout/chat_room_detail_fragment.xml b/app/src/main/res/layout/chat_room_detail_fragment.xml index 1f3bbd365..9b305bfba 100644 --- a/app/src/main/res/layout/chat_room_detail_fragment.xml +++ b/app/src/main/res/layout/chat_room_detail_fragment.xml @@ -124,7 +124,7 @@ android:layout_weight="0.2" android:background="?attr/button_background_drawable" android:padding="15dp" - android:src="@drawable/menu_more"/> + android:src="@drawable/icon_menu_more"/> + android:layout_marginRight="8dp" + android:adjustViewBounds="true" + android:contentDescription="@{viewModel.securityLevelContentDescription}" + android:onClick="@{securityIconClickListener}" + android:src="@{viewModel.securityLevelIcon, default=@drawable/security_alert_indicator}" + android:visibility="@{viewModel.encryptedChatRoom ? View.VISIBLE : View.GONE}" /> - - - - - + android:paddingBottom="8dp" + android:layout_marginTop="10dp" + android:layout_marginBottom="10dp" + android:layout_gravity="center" + android:background="@drawable/shape_rect_orange_button" + android:text="@string/chat_room_group_info_leave" + style="@style/big_orange_button_font"/> + bind:visibility="@{viewModel.waitForChatRoomCreation}" /> diff --git a/app/src/main/res/layout/chat_room_group_info_participant_cell.xml b/app/src/main/res/layout/chat_room_group_info_participant_cell.xml index 287abd3b5..0fab23c90 100644 --- a/app/src/main/res/layout/chat_room_group_info_participant_cell.xml +++ b/app/src/main/res/layout/chat_room_group_info_participant_cell.xml @@ -47,29 +47,29 @@ android:src="@drawable/chat_group_delete" /> + android:layout_toLeftOf="@id/delete" + android:visibility="@{data.showAdminControls && data.canBeSetAdmin ? View.VISIBLE : View.GONE}"> + android:layout_marginRight="20dp" + android:onClick="@{() -> data.unSetAdmin()}" + android:visibility="@{data.admin ? View.VISIBLE : View.GONE}"> + android:layout_marginRight="20dp" + android:onClick="@{() -> data.setAdmin()}" + android:visibility="@{data.admin ? View.GONE : View.VISIBLE}"> + android:maxLines="2" + android:text="@{viewModel.lastMessageText}" /> - + @@ -38,9 +39,9 @@ android:visibility="@{groupInfoHidden ? View.GONE : View.VISIBLE}" android:background="@drawable/menu_background" android:onClick="@{groupInfoListener}" - android:drawableRight="@drawable/chat_room_menu_group_info" style="@style/popup_item_font" - android:text="@string/chat_room_context_menu_group_info" /> + android:text="@string/chat_room_context_menu_group_info" + app:drawableRightCompat="@drawable/chat_room_menu_group_info" /> + android:text="@string/chat_room_context_menu_participants_devices" + app:drawableRightCompat="@drawable/chat_room_menu_devices" /> + android:text="@string/chat_message_context_menu_ephemeral_messages" + app:drawableRightCompat="@drawable/chat_room_menu_ephemeral" /> + android:text="@string/chat_message_context_menu_delete_messages" + app:drawableRightCompat="@drawable/chat_room_menu_delete" /> diff --git a/app/src/main/res/layout/conference_paused.xml b/app/src/main/res/layout/conference_paused.xml deleted file mode 100644 index 4b47e69d6..000000000 --- a/app/src/main/res/layout/conference_paused.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/conference_schedule_cell.xml b/app/src/main/res/layout/conference_schedule_cell.xml new file mode 100644 index 000000000..2968a25b3 --- /dev/null +++ b/app/src/main/res/layout/conference_schedule_cell.xml @@ -0,0 +1,285 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/conference_schedule_list_header.xml b/app/src/main/res/layout/conference_schedule_list_header.xml new file mode 100644 index 000000000..55c3f8434 --- /dev/null +++ b/app/src/main/res/layout/conference_schedule_list_header.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/conference_scheduling_fragment.xml b/app/src/main/res/layout/conference_scheduling_fragment.xml new file mode 100644 index 000000000..042e8ae4d --- /dev/null +++ b/app/src/main/res/layout/conference_scheduling_fragment.xml @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/conference_scheduling_participant_cell.xml b/app/src/main/res/layout/conference_scheduling_participant_cell.xml new file mode 100644 index 000000000..38fc01fb2 --- /dev/null +++ b/app/src/main/res/layout/conference_scheduling_participant_cell.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/conference_scheduling_participants_list_fragment.xml b/app/src/main/res/layout/conference_scheduling_participants_list_fragment.xml new file mode 100644 index 000000000..6ca7d29f7 --- /dev/null +++ b/app/src/main/res/layout/conference_scheduling_participants_list_fragment.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/conference_scheduling_summary_fragment.xml b/app/src/main/res/layout/conference_scheduling_summary_fragment.xml new file mode 100644 index 000000000..5059918a4 --- /dev/null +++ b/app/src/main/res/layout/conference_scheduling_summary_fragment.xml @@ -0,0 +1,324 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/conference_waiting_room_fragment.xml b/app/src/main/res/layout/conference_waiting_room_fragment.xml new file mode 100644 index 000000000..b172506c4 --- /dev/null +++ b/app/src/main/res/layout/conference_waiting_room_fragment.xml @@ -0,0 +1,229 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/conferences_scheduled_fragment.xml b/app/src/main/res/layout/conferences_scheduled_fragment.xml new file mode 100644 index 000000000..04b3d7211 --- /dev/null +++ b/app/src/main/res/layout/conferences_scheduled_fragment.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/contact_avatar.xml b/app/src/main/res/layout/contact_avatar.xml index cde6ae382..98c03b62a 100644 --- a/app/src/main/res/layout/contact_avatar.xml +++ b/app/src/main/res/layout/contact_avatar.xml @@ -46,7 +46,7 @@ android:layout_alignParentLeft="true" android:adjustViewBounds="true" android:contentDescription="@null" - android:src="@{groupChatAvatarVisibility ? @drawable/chat_group_avatar : @drawable/avatar}"/> + android:src="@{groupChatAvatarVisibility ? @drawable/voip_multiple_contacts_avatar : @drawable/voip_single_contact_avatar}"/> + android:src="@drawable/voip_single_contact_avatar"/> + type="org.linphone.contact.ContactsSelectionViewModel" /> + type="org.linphone.contact.ContactSelectionData" /> + @@ -66,8 +69,20 @@ android:layout_height="60dp" android:layout_alignParentBottom="true"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/history_detail_cell.xml b/app/src/main/res/layout/history_detail_cell.xml index b2aadd39a..f682ba7a3 100644 --- a/app/src/main/res/layout/history_detail_cell.xml +++ b/app/src/main/res/layout/history_detail_cell.xml @@ -5,7 +5,7 @@ + type="org.linphone.activities.main.history.viewmodels.CallLogViewModel" /> + type="org.linphone.activities.main.history.viewmodels.CallLogViewModel" /> @@ -98,7 +98,7 @@ - - + @@ -43,17 +38,17 @@ android:layout_weight="0.2"> + + + + + + + + + android:layout_weight="0.2" /> + android:visibility="@{viewModel.displayedCallLogs.empty && viewModel.filter == CallLogsFilter.ALL ? View.VISIBLE : View.GONE}" /> + android:visibility="@{viewModel.displayedCallLogs.empty && viewModel.filter == CallLogsFilter.MISSED ? View.VISIBLE : View.GONE}" /> + + + android:layout_height="match_parent"> + + + + diff --git a/app/src/main/res/layout/settings_call_fragment.xml b/app/src/main/res/layout/settings_call_fragment.xml index 97c901951..827758b7a 100644 --- a/app/src/main/res/layout/settings_call_fragment.xml +++ b/app/src/main/res/layout/settings_call_fragment.xml @@ -112,13 +112,6 @@ linphone:listener="@{viewModel.useTelecomManagerListener}" linphone:checked="@={viewModel.useTelecomManager}"/> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/settings_fragment.xml b/app/src/main/res/layout/settings_fragment.xml index 7f2bd98a7..a46a6b9b4 100644 --- a/app/src/main/res/layout/settings_fragment.xml +++ b/app/src/main/res/layout/settings_fragment.xml @@ -153,6 +153,13 @@ linphone:title="@{@string/settings_call_title}" linphone:icon="@{@drawable/settings_call}" /> + + + @@ -50,7 +53,7 @@ android:layout_height="match_parent" android:adjustViewBounds="true" android:padding="10dp" - android:src="@drawable/avatar" + android:src="@drawable/voip_single_contact_avatar" android:contentDescription="@string/content_description_change_own_picture" glideAvatarFallback="@{viewModel.defaultAccountAvatar}" /> @@ -162,6 +165,7 @@ @@ -191,6 +195,7 @@ @@ -220,6 +225,37 @@ + + + + + + + + + + @@ -249,6 +285,7 @@ diff --git a/app/src/main/res/layout/voip_active_call_or_conference_fragment.xml b/app/src/main/res/layout/voip_active_call_or_conference_fragment.xml new file mode 100644 index 000000000..4e5f52de4 --- /dev/null +++ b/app/src/main/res/layout/voip_active_call_or_conference_fragment.xml @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_activity.xml b/app/src/main/res/layout/voip_activity.xml new file mode 100644 index 000000000..8a2f06742 --- /dev/null +++ b/app/src/main/res/layout/voip_activity.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_buttons.xml b/app/src/main/res/layout/voip_buttons.xml new file mode 100644 index 000000000..cfba92728 --- /dev/null +++ b/app/src/main/res/layout/voip_buttons.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_buttons_audio_routes.xml b/app/src/main/res/layout/voip_buttons_audio_routes.xml new file mode 100644 index 000000000..67b97794e --- /dev/null +++ b/app/src/main/res/layout/voip_buttons_audio_routes.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_buttons_extra.xml b/app/src/main/res/layout/voip_buttons_extra.xml new file mode 100644 index 000000000..b79f433d4 --- /dev/null +++ b/app/src/main/res/layout/voip_buttons_extra.xml @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_buttons_incoming.xml b/app/src/main/res/layout/voip_buttons_incoming.xml new file mode 100644 index 000000000..c9acbf3ab --- /dev/null +++ b/app/src/main/res/layout/voip_buttons_incoming.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_buttons_outgoing.xml b/app/src/main/res/layout/voip_buttons_outgoing.xml new file mode 100644 index 000000000..99a7ab394 --- /dev/null +++ b/app/src/main/res/layout/voip_buttons_outgoing.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_call.xml b/app/src/main/res/layout/voip_call.xml new file mode 100644 index 000000000..e299c6049 --- /dev/null +++ b/app/src/main/res/layout/voip_call.xml @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_call_context_menu.xml b/app/src/main/res/layout/voip_call_context_menu.xml new file mode 100644 index 000000000..510245543 --- /dev/null +++ b/app/src/main/res/layout/voip_call_context_menu.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_call_incoming_fragment.xml b/app/src/main/res/layout/voip_call_incoming_fragment.xml new file mode 100644 index 000000000..99449cf91 --- /dev/null +++ b/app/src/main/res/layout/voip_call_incoming_fragment.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_call_outgoing_fragment.xml b/app/src/main/res/layout/voip_call_outgoing_fragment.xml new file mode 100644 index 000000000..3ab17e027 --- /dev/null +++ b/app/src/main/res/layout/voip_call_outgoing_fragment.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_call_paused.xml b/app/src/main/res/layout/voip_call_paused.xml new file mode 100644 index 000000000..1bbc392c6 --- /dev/null +++ b/app/src/main/res/layout/voip_call_paused.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_call_paused_by_remote.xml b/app/src/main/res/layout/voip_call_paused_by_remote.xml new file mode 100644 index 000000000..a003842cf --- /dev/null +++ b/app/src/main/res/layout/voip_call_paused_by_remote.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/call_single_statistic_cell.xml b/app/src/main/res/layout/voip_call_stat_cell.xml similarity index 77% rename from app/src/main/res/layout/call_single_statistic_cell.xml rename to app/src/main/res/layout/voip_call_stat_cell.xml index 2d9c44362..99a1b2ba6 100644 --- a/app/src/main/res/layout/call_single_statistic_cell.xml +++ b/app/src/main/res/layout/voip_call_stat_cell.xml @@ -5,12 +5,13 @@ + type="org.linphone.activities.voip.data.StatItemData" /> + android:gravity="center" /> + android:gravity="center" /> diff --git a/app/src/main/res/layout/voip_call_stats.xml b/app/src/main/res/layout/voip_call_stats.xml new file mode 100644 index 000000000..d7f148140 --- /dev/null +++ b/app/src/main/res/layout/voip_call_stats.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/call_statistics_cell.xml b/app/src/main/res/layout/voip_call_stats_cell.xml similarity index 71% rename from app/src/main/res/layout/call_statistics_cell.xml rename to app/src/main/res/layout/voip_call_stats_cell.xml index 458ffa243..07c1c8975 100644 --- a/app/src/main/res/layout/call_statistics_cell.xml +++ b/app/src/main/res/layout/voip_call_stats_cell.xml @@ -7,7 +7,7 @@ + type="org.linphone.activities.voip.data.CallStatisticsData" /> - - - - + app:layout="@{@layout/voip_call_stat_cell}"/> - - + app:layout="@{@layout/voip_call_stat_cell}" /> diff --git a/app/src/main/res/layout/voip_calls_cell.xml b/app/src/main/res/layout/voip_calls_cell.xml new file mode 100644 index 000000000..2cbd37552 --- /dev/null +++ b/app/src/main/res/layout/voip_calls_cell.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_calls_list_fragment.xml b/app/src/main/res/layout/voip_calls_list_fragment.xml new file mode 100644 index 000000000..9aad7b9ed --- /dev/null +++ b/app/src/main/res/layout/voip_calls_list_fragment.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_chat_fragment.xml b/app/src/main/res/layout/voip_chat_fragment.xml new file mode 100644 index 000000000..a83e49fed --- /dev/null +++ b/app/src/main/res/layout/voip_chat_fragment.xml @@ -0,0 +1,202 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_conference_active_speaker.xml b/app/src/main/res/layout/voip_conference_active_speaker.xml new file mode 100644 index 000000000..ee0d898b8 --- /dev/null +++ b/app/src/main/res/layout/voip_conference_active_speaker.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_conference_creation_pending_wait_layout.xml b/app/src/main/res/layout/voip_conference_creation_pending_wait_layout.xml new file mode 100644 index 000000000..e0aa9338e --- /dev/null +++ b/app/src/main/res/layout/voip_conference_creation_pending_wait_layout.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_conference_grid.xml b/app/src/main/res/layout/voip_conference_grid.xml new file mode 100644 index 000000000..f267ac657 --- /dev/null +++ b/app/src/main/res/layout/voip_conference_grid.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_conference_layout_fragment.xml b/app/src/main/res/layout/voip_conference_layout_fragment.xml new file mode 100644 index 000000000..058dec71f --- /dev/null +++ b/app/src/main/res/layout/voip_conference_layout_fragment.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_conference_participant_cell.xml b/app/src/main/res/layout/voip_conference_participant_cell.xml new file mode 100644 index 000000000..c986a571f --- /dev/null +++ b/app/src/main/res/layout/voip_conference_participant_cell.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_conference_participant_remote_active_speaker_miniature.xml b/app/src/main/res/layout/voip_conference_participant_remote_active_speaker_miniature.xml new file mode 100644 index 000000000..d053de2e4 --- /dev/null +++ b/app/src/main/res/layout/voip_conference_participant_remote_active_speaker_miniature.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_conference_participant_remote_grid.xml b/app/src/main/res/layout/voip_conference_participant_remote_grid.xml new file mode 100644 index 000000000..924ffaf1f --- /dev/null +++ b/app/src/main/res/layout/voip_conference_participant_remote_grid.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_conference_participants_add_fragment.xml b/app/src/main/res/layout/voip_conference_participants_add_fragment.xml new file mode 100644 index 000000000..039559fdc --- /dev/null +++ b/app/src/main/res/layout/voip_conference_participants_add_fragment.xml @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_conference_participants_fragment.xml b/app/src/main/res/layout/voip_conference_participants_fragment.xml new file mode 100644 index 000000000..d277ad0f1 --- /dev/null +++ b/app/src/main/res/layout/voip_conference_participants_fragment.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_conference_paused.xml b/app/src/main/res/layout/voip_conference_paused.xml new file mode 100644 index 000000000..62528a2f3 --- /dev/null +++ b/app/src/main/res/layout/voip_conference_paused.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_contact_avatar.xml b/app/src/main/res/layout/voip_contact_avatar.xml new file mode 100644 index 000000000..558e7cce5 --- /dev/null +++ b/app/src/main/res/layout/voip_contact_avatar.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_contact_avatar_alt.xml b/app/src/main/res/layout/voip_contact_avatar_alt.xml new file mode 100644 index 000000000..3be1438c0 --- /dev/null +++ b/app/src/main/res/layout/voip_contact_avatar_alt.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_dialog.xml b/app/src/main/res/layout/voip_dialog.xml new file mode 100644 index 000000000..85b4473db --- /dev/null +++ b/app/src/main/res/layout/voip_dialog.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_dialog_info.xml b/app/src/main/res/layout/voip_dialog_info.xml new file mode 100644 index 000000000..0a1bc924b --- /dev/null +++ b/app/src/main/res/layout/voip_dialog_info.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_numpad.xml b/app/src/main/res/layout/voip_numpad.xml new file mode 100644 index 000000000..93cfe7873 --- /dev/null +++ b/app/src/main/res/layout/voip_numpad.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/call_status_fragment.xml b/app/src/main/res/layout/voip_status_fragment.xml similarity index 82% rename from app/src/main/res/layout/call_status_fragment.xml rename to app/src/main/res/layout/voip_status_fragment.xml index 501a0fb58..76341c9de 100644 --- a/app/src/main/res/layout/call_status_fragment.xml +++ b/app/src/main/res/layout/voip_status_fragment.xml @@ -3,15 +3,12 @@ - + type="org.linphone.activities.voip.viewmodels.StatusViewModel" /> + android:contentDescription="@{viewModel.callQualityContentDescription}" + android:padding="10dp" + android:src="@{viewModel.callQualityIcon, default=@drawable/call_quality_indicator_0}" /> @@ -52,9 +49,10 @@ android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_centerVertical="true" - android:layout_toRightOf="@id/status_led" + android:layout_toEndOf="@id/status_led" android:gravity="center_vertical" - android:paddingLeft="5dp" /> + android:paddingStart="5dp" + android:paddingEnd="5dp" /> diff --git a/app/src/main/res/layout/wait_layout.xml b/app/src/main/res/layout/wait_layout.xml index b74fd9680..976ee049c 100644 --- a/app/src/main/res/layout/wait_layout.xml +++ b/app/src/main/res/layout/wait_layout.xml @@ -22,7 +22,7 @@ android:layout_height="wrap_content" android:layout_centerInParent="true" android:indeterminate="true" - android:indeterminateTint="?attr/accentColor"/> + android:indeterminateTint="?attr/accentColor" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/contacts_nav_graph.xml b/app/src/main/res/navigation/contacts_nav_graph.xml index 7d2d06d68..500609dbf 100644 --- a/app/src/main/res/navigation/contacts_nav_graph.xml +++ b/app/src/main/res/navigation/contacts_nav_graph.xml @@ -2,7 +2,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml index 5a9df56af..7b1ad74dc 100644 --- a/app/src/main/res/navigation/main_nav_graph.xml +++ b/app/src/main/res/navigation/main_nav_graph.xml @@ -2,7 +2,6 @@ @@ -52,15 +50,12 @@ android:id="@+id/action_masterContactsFragment_to_masterChatRoomsFragment" app:destination="@id/masterChatRoomsFragment" /> @@ -129,13 +124,13 @@ tools:layout="@layout/settings_fragment" android:label="SettingsFragment" > + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/settings_nav_graph.xml b/app/src/main/res/navigation/settings_nav_graph.xml index 3e226bf78..1da8f94e0 100644 --- a/app/src/main/res/navigation/settings_nav_graph.xml +++ b/app/src/main/res/navigation/settings_nav_graph.xml @@ -2,7 +2,6 @@ + @@ -134,4 +136,9 @@ android:name="IsLinking" app:argType="boolean" /> + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index cbd864f2f..a1fd30e79 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -8,7 +8,6 @@ Datenschutz-Bestimmungen Lagdatei wurde gelöscht Logdatei-URL wurde in die Zwischenablage kopiert - Fehler Telefonnummer Telefonnummern Wähle dein Land @@ -111,7 +110,6 @@ Anruf pausiert Ausgehender Anruf Laufender Anruf - Konferenz Gesprächspartner möchte das Video einschalten Inkompatible Medienparameter Netzwerk nicht erreichbar @@ -119,7 +117,6 @@ &appName; wurde automatich gestartet Hochladen der Logdatei fehlgeschlagen! Ländername oder Vorwahl - Ungültige E-Mail-Adresse der freie SIP client Diesen Eintrag löschen\? Hallo, benutze &appName;! Du kannst es kostenlos hier %s herunterladen @@ -131,7 +128,6 @@ %s ist kein Administrator mehr Teilnehmer Du bist jetzt Administrator - Unterhaltung auswählen oder eine neue beginnen Nochmal senden Die Nachricht in diesen Raum weiterleiten\? Nachrichten löschen @@ -145,7 +141,6 @@ Bestätigungscode Weiter SIP-Konto benutzen - Das Konto wurde noch nicht überprüft Das Konto existiert nicht oder das Passwort ist falsch Den Benutzernamen und das Passwort anstatt der Telefonnummer verwenden Bitte den Benutzernamen und das Passwort des &appName;-Kontos eingeben @@ -180,8 +175,6 @@ Domäne Erweitert Ersetze + durch 00 - Nach rechts streichen um den Anruf zu beenden - Nach links streichen um den Anruf anzunehmen Anruf beantworten Mikrofon stummschalten Audioausgabe auf Lautsprecher diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 1f6dea08d..03c558a5b 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -9,7 +9,6 @@ Esto es necesario para recibir llamadas en segundo plano &appName; contactos Visite nuestra política de privacidad - Error Número de teléfono Números de teléfonos Selecciona tu país @@ -32,15 +31,12 @@ %d días Compartir enlace de registros usando… - Email inválido Hoy Ayer Transporte UDP TCP TLS - MD5 - SHA-256 El cliente libre Conectado No conectado @@ -115,27 +111,19 @@ Muestra para qué se usará tu número de teléfono Vacía el campo de texto que está a su lado Cambiar de la cámara delantera a la trasera y viceversa - Retirar al participante de la conferencia - Pausar la conferencia - Su llamada ha sido puesta en espera Pausar la llamada Habilitar o deshabilitar la grabación de la llamada Termina la llamada - Deslíce a la izquierda para contestar la llamada Contestar la llamada Silencie su micrófono Activar el micrófono Dirigir el audio al altavoz Volver a la vista de llamadas - Reanudar la llamada actualmente en espera - Reanudar la conferencia actualmente en espera - Mostrar la lista de conversaciones Habilitar el video en la llamada Deshabilitar el video en la llamada Mostrar u ocultar el menú de salida de audio Usar el auricular como salida de audio Usar el altavoz como salida de audio - Mostrar u ocultar el menú de acciones avanzadas Iniciar nueva llamada Transferir la llamada actual a otra persona Eliminar la transferencia de archivos pendiente @@ -168,7 +156,6 @@ Guardar los cambios Cambiar foto de contacto Cambiar mi propia foto - Dirección de llamada Salir del modo de edición Seleccionar todos los temas de la lista Deseleccionar todos los elementos de la lista @@ -209,25 +196,19 @@ Calidad de llamada máxima La llamada no está asegurada La llamada está asegurada - Mostrar u ocultar las estadísticas de llamadas Video adjunto Estás seguro que quieres borrar estas conversaciones\? - Fusionar todas las llamadas en una conferencia Llamada saliente - Deslíce a la derecha para terminar la llamada Llamada ha sido declinada Dirigir el audio al auricular El corresponsal quisiera encender el video - Mostrar u ocultar los dígitos para enviar DTMFs Resolución de video recibida: Usar auriculares bluetooth como salida de audio - Filtro de pantalla: Este asistente le ayudará a configurar y utilizar su cuenta SIP. Pendiente de la transferencia de archivos Contacto es seleccionado Mostrat una lsita de todos los contactos SIP Adjunte un archivo al mensaje - \nGracias a tu número de teléfono, tus amigos te encontrarán más fácilmente.\n\n Verás en tu libreta de direcciones quién está usando &appName; y tus amigos sabrán que pueden contactarte en &appName; también.\n Cambiar la duración efímera por el valor seleccionado El contacto es un administrador en esta conversación Sólo puedes usar tu número de teléfono con una cuenta de &appName;.\n\nSi ya habías vinculado tu número a otra cuenta pero prefieres usar esta, simplemente vincúlala ahora y tu número se moverá automáticamente a esta cuenta. @@ -266,7 +247,6 @@ Sólo se mostrará el nombre del remitente Crear atajos para las salas de chat en el lanzador Esconder las salas de chat de las configuraciones de proxy eliminadas - Usar los parámetros de notificación de los envíos anteriores Insertar atajos de información desde el &appName; contacto en los contactos nativos de Android Si el contacto es deshabilitado se almacena localmente Nombre de dispositivo @@ -308,8 +288,6 @@ Borrar Agregar a contactos Quieres reenviar el mensaje en esta sala\? - Selecciona una conversación o crea una nueva - La creación de la sala de chat falló Inforamción del grupo Conversaciones en estos dispositivos Mensajes efímeros @@ -331,11 +309,9 @@ <Redactado> No hay grabaciones Llamada entrante - La llamada ha sido interrumpida por su corresponsal Llamada pausada Llamada saliente Llamada en curso - Conferencia Usuario está ocupado El usuario no ha sido encontrado Parámetros de medios incompatibles @@ -372,7 +348,6 @@ Las contraseñas no coinciden La dirección de correo electrónico es inválida El nombre de usuario tiene demasiados caracteres - Su cuenta no ha sido validada todavía Use su &appName; cuenta Por favor, introduzca su nombre de usuario y contraseña de &appName; cuenta Usa tu nombre de usuario y contraseña en lugar de tu número de teléfono @@ -384,7 +359,6 @@ Usar cuenta SIP Por favor, introduzca su nombre de usuario y contraseña con su dominio SIP Mostrar nombre (opcional) - Algoritmo hash Confirmación de contraseña Su cuenta está creada. Por favor, comprueba tus correos para validar tu cuenta: Una vez hecho, vuelve aquí y pulsa el botón. @@ -471,8 +445,6 @@ Configuración de notificación de Android Usar sólo WiFi Permitir IPv6 - Habilitar noticiaciones push - Requerido cuando se usa Flexisip < 2.0 Usar puertos aleatorios Puerto SIP a usar Suscribirse a la lista de amigos @@ -541,7 +513,6 @@ &appName; notificaciones de mensajes Llamada entrante Estado de envío - No se puede abrir el archivo, no hay una apliación disponible para este formato. Los mensajes instantáneos se cifran de extremo a extremo en conversaciones seguras. Es posible mejorar el nivel de seguridad de una conversación autenticando a los participantes. Para ello, llame al contacto y siga el proceso de autenticación. Enviar registros Restablecer registros @@ -563,12 +534,9 @@ Servidor timeout condiciones de uso Acepto Belledonne Communications\' %1$s and %2$s - pantalla completa durante una llamada Mostrar overlay fuera de la aplicación - Habilitar mensajes efímeros (beta) &appName; notificaciones de llamadas perdidas Reenviar mensaje en esta conversación - Tomar una captura de pantalla del video recibido Ver archivo de configuración Seleccione o cree una conversación para reenviar el mensaje Seleccione o cree una conversación para compartir los archivos @@ -581,10 +549,8 @@ Exportar Abrir como texto Temporalmente no disponible - Erro política de privacidad Cuenta principal - Oculta las barras de status y navegación Redirigir las llamadas rechazadas al correo de voz Sonar en los primeros entrantes Pausar llamadas cuando se pierde el enfoque del audio @@ -596,7 +562,6 @@ Cancelar Dirigir el audio al dispositivo bluetooth si existe El mensaje es una respuesta - Archivo adjunto al mensaje Cancelar la respuesta Respuesta Responder diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index df0cff122..c9f057d54 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -10,21 +10,15 @@ Codec : La calibration de l\'annulateur d\'écho est en cours Mettre fin à l\'appel - Faire glisser vers la droite pour raccrocher - Faire glisser vers la gauche pour décrocher Décrocher Couper le micro Réactiver le micro Contrôle qualité adaptatif Afficher la caméra sur la vue d\'accueil - Reprendre l\'appel - Reprendre la conférence - Afficher la liste des conversations Désactiver la vidéo en appel Activer la vidéo en appel Utiliser un casque Bluetooth comme périphérique de sortie Utiliser des écouteurs comme périphérique de sortie - Afficher ou masquer le menu des actions avancées Nouvel appel Transférer l\'appel Transfert d\'appel en attente @@ -43,7 +37,6 @@ Contacts &appName; Consulter notre politique de confidentialité URL des traces copiée dans le presse-papier - Erreur Numéro de téléphone Numéros de téléphone Préfixe @@ -56,7 +49,6 @@ Adresse SIP Initier les appels en vidéo Annuler - Email non valide Aujourd\'hui Hier Transport @@ -64,8 +56,6 @@ Toujours envoyer une demande d\'activation de la vidéo TCP TLS - MD5 - SHA-256 Le client SIP open source Connecté Non connecté @@ -132,9 +122,6 @@ Supprimer Toujours accepter d\'activer la vidéo Accepter d\'activer la vidéo - Sélectionnez une conversation ou créez-en une nouvelle - Impossible d\'ouvrir le fichier, aucune application disponible pour ce format. - La conversation n\'a pas pu être créée Infos du groupe Terminaux de la conversation Messages éphémères @@ -152,11 +139,9 @@ Aucun enregistrement disponible Appel entrant Appel sortant - L\'appel a été mis en pause par votre interlocuteur Appel en pause Appel sortant Appel en cours - Conférence L\'appel a été refusé L\'utilisateur est occupé L\'utilisateur n\'a pu être trouvé @@ -196,7 +181,6 @@ Les mots de passe ne correspondent pas L\'adresse email est invalide Le nom d\'utilisateur contient trop de caractères - Votre compte n\'a pas encore été activé Passer Lier le compte Télécharger la configuration distante @@ -223,7 +207,6 @@ Continuer Utiliser un compte SIP Veuillez saisir votre nom d\'utilisateur et votre mot de passe avec le domaine SIP - Algorithme de hachage Veuillez saisir un nom d\'utilisateur, un email et un mot de passe pour votre compte &appName; Confirmation du mot de passe Utiliser un nom d\'utilisateur (optionnel) @@ -269,17 +252,14 @@ Requis par le mode double Exemple : john, si votre compte est john@sip.example.org Vous devrez re-saisir votre mot de passe si vous changez votre nom d\'utilisateur ou le domaine - Utiliser les anciens paramètres de notifications push Notification de service &appName; Notifications de messages entrants &appName; Faire passer tous les appels via le serveur mandataire SIP Intervalle AVPF RTCP régulier Ne plus afficher Dans les conversations sécurisées, les messages sont chiffrés de bout en bout. Il est possible d\'augmenter le niveau de sécurité en vérifiant l\'identité des participants. Pour ce faire, appelez le contact et suivez la procédure d\'authentification. - Retirer le participant de la conférence Activer ou désactiver l\'enregistrement d\'appel Revenir à la vue d\'appel - Afficher ou masquer le pavé numérique pour envoyer des DTMFs Utiliser une enceinte comme périphérique de sortie Le message a été transféré Le chiffrement de bout-en-bout est activé @@ -328,7 +308,6 @@ Paramètres de notification Android Utiliser uniquement la connexion Wi-Fi Autoriser IPv6 - Requis quand vous utilisez un serveur Flexisip < 2.0 Utiliser des ports aléatoires Port SIP à utiliser Information de présence dans le contact natif @@ -338,7 +317,6 @@ Toujours demander dans quel compte le nouveau contact doit être ajouté Si désactivé, le contact sera enregistré localement Traces de déboggage - Activer les notifications push Mode arrière-plan Affiche une notification pour maintenir la connexion avec l\'application Démarrer au lancement du téléphone @@ -427,9 +405,6 @@ Revenir en arrière Affiche à quoi le numéro de téléphone va-t-il servir Intervertir la source de capture de la vidéo - Mettre la conférence en pause - Fusionner les appels dans une conférence - Votre appel a été mis en pause Mettre l\'appel en pause Editer Ajouter une pièce jointe @@ -449,7 +424,6 @@ Sauvegarder les modifications Modifier la photo du contact Modifier ma photo - Direction d\'appel Aller à la vue de détails de l\'appel Tout sélectionner Tout désélectionner @@ -490,7 +464,6 @@ L\'appel n\'est pas chiffré Voulez-vous supprimer cet enregistrement \? Le chiffrement est en cours - Afficher ou masquer les statistiques d\'appel Vidéo en pièce jointe Aucun compte configuré Bonjour ! Rejoins-moi sur &appName;! Disponible gratuitement ici : %s @@ -524,9 +497,7 @@ Bande passante d\'envoi : Connectivité ICE : Résolution vidéo envoyée : - Filtre d\'affichage : Cet assistant va vous aider à configurer et utiliser votre compte SIP. - \nGrâce à votre numéro de téléphone, vos amis vous trouverons plus facilement.\n\nVous verrez dans votre carnet d\'adresses qui utilise &appName; et vos amis verront également qu\'ils peuvent vous contacter via &appName;.\n \nVos amis pourront vous joindre plus facilement si vous associez votre compte à votre numéro de téléphone.\n\nVous verrez dans votre carnet d\'adresses les contacts qui utilisent &appName; et vos amis sauront qu\'ils peuvent vous contacter.\n Le compte n\'existe pas ou les mots de passe ne correspondent pas Veuillez saisir le nom d\'utilisateur et mot de passe de votre compte &appName; @@ -544,7 +515,6 @@ Transport Vider le champ texte à côté Notifications d\'appels manqués &appName; - Prendre une capture d\'écran de la vidéo reçue les conditions d\'utilisation Fermer la bulle de notification la politique de confidentialité @@ -569,8 +539,6 @@ Autres paramètres Afficher la notification flottante en dehors de l\'app Quand un nombre est saisi, applique le préfixe - Plein écran pendant un appel - Cache la barre de status et de navigation Fichier non trouvé Applique le préfixe pour les appels sortants et les messages Compte principal @@ -589,14 +557,11 @@ Sélectionnez ou créez une conversation pour transférer le message Sélectionnez ou créez une conversation pour partager le(s) fichier(s) Sélectionnez ou créez une conversation pour partager le texte - Messages éphémères (bêta) Répondre Réponse Message Réponse Gardez le bouton enfoncé pour enregistrer un message vocal - Fichier attaché au message - Enregistrer un message vocal Annuler la réponse Enregistrer un message vocal Annuler l\'enregistrement du message vocal diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 01937613c..0f773030b 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -8,7 +8,6 @@ A naplók URL-je a vágólapra másolva Az adatvédelmi szabályzatunkat megtekintése &appName; névjegyek - Hiba Telefonszám Ország/régió kiválasztása Nemzetközi előhívószám @@ -24,15 +23,12 @@ %d nap %d nap - Érvénytelen e-mail-cím Ma Tegnap Forgalom UDP TCP TLS - MD5 - SHA-256 a szabad SIP-ügyfél Csatlakoztatva Nincs csatlakozva @@ -99,7 +95,6 @@ Kézbesítés állapota Törlés Fájl nem található - A csevegőszoba létrehozása sikertelen Csoport adatai Üzenetek törlése Elmúló üzenetek @@ -125,11 +120,8 @@ %s írnak… Szeretné továbbítani az üzenetet ebben a szobában\? - Beszélgetés kiválasztása vagy új létrehozása - Nem lehet megnyitni a fájlt, ehhez a formátumhoz nem áll rendelkezésre alkalmazás. Kimenő hívás Kimenő hívás - Konferencia Felhasználó elfoglalt Hálózat elérhetetlen A szolgáltatás nem érhető el vagy hálózati hiba történt @@ -154,14 +146,12 @@ Vibrálás tartalék: Kódoló: Lejátszói szűrő: - Megjelenítési szűrő: Rögzítési szűrő: Isten hozott! Fiók létrehozása &appName;-fiók használata SIP-fiók használata Távoli beállítások lekérése - A hívott fél szüneteltette a hívását Hogyan fog a telefonszámom használódni\? Telefonszámát csak egy &appName;-fiókjával használhatja.\n\nHa már összekapcsolta telefonszámát egy másik fiókkal, de inkább ezt használja, egyszerűen kapcsolja össze most, és a telefonszám önműködően átkerül erre a fiókra. Ez a felhasználónév már foglalt @@ -180,7 +170,6 @@ Visszaigazolási kód SIP-fiók használata Kérjük, adja meg felhasználónevét és jelszavát a SIP-tartományhoz - Kivonatoló algoritmus Jelszó megerősítése Kérjük, adjon meg egy felhasználónevet, e-mail-címet és jelszót a(z) &appName;-fiókhoz Kihagyás @@ -237,7 +226,6 @@ ZRTP DTLS Média titkosítása kötelező - Teljes képernyős alkalmazás hívás közben Hívás értesítés átfedése Átfedés megjelenítése az alkalmazáson kívül Megkérjük, hogy adjon átfedési engedélyt @@ -311,7 +299,6 @@ Hívásfogadás Megjelölés olvasottként Nem fogadott hívás - Távolítsa el a résztvevőt a konferenciáról Erősítse meg a következő SAS-kódot a hívott felével Elfogad Elutasítás @@ -323,13 +310,11 @@ Elfogad Elutasítás Hívás - Szüneteltesse a konferenciát Szüneteltesse a hívást Megszakítja a hívást Fogadja a hívást Mikrofonjának némításának feloldása Menjen vissza a hívás nézethez - Beszélgetések listájának megjelenítése A kapcsolattartó nem felügyelő ebben a beszélgetésben Az üzenetek elmúló ebben a beszélgetésben Elmúló időtartam kiválasztva @@ -341,7 +326,6 @@ Hangkimeneti menü megjelenítése vagy elrejtése Használja a Bluetooth-fejbeszélőt hang kimenetként Használja a fülhallgatót hang kimenetként - Speciális műveletek menü megjelenítése vagy elrejtése Új hívás indítása Az aktuális hívás átadása másnak Fájl átvitele függőben van @@ -365,7 +349,6 @@ Változtatások elvetése Névjegyfénykép módosítása Saját névjegyfénykép módosítása - Hívásirány &appName; szolgáltatás A(z) &appName; önműködően elindult Telefonszámok @@ -405,11 +388,9 @@ Végpontok közötti titkosítás engedélyezve Kattintson a visszhangkioltás beállításának megkezdéséhez Kattintson a visszhang ellenőrzés indításához - \nTelefonszámának köszönhetően ismerősei könnyebben megtalálják Önt.\n\nA címjegyzékében látni fogja, hogy ki használja a(z) &appName; és az ismerősök tudni fogják, hogy elérhetik Önt a(z) &appName; is.\n \nAz ismerősei könnyebben megtalálja Önt, ha összekapcsolja fiókját a telefonszámával.\n\nA címjegyzékében látni fogja, hogy ki használja a(z) &appName; és az ismerősök tudni fogják, hogy elérhetik Önt a(z) &appName; is.\n Ismerőslista feliratkozása Kérjük, adja meg a(z) &appName;-fiókjának felhasználónevét és jelszavát - Fiókját még nem érvényesítettük Országának nemzetközi előhívószáma Kérjük, erősítse meg nemzetközi előhívószámát, és adja meg telefonszámát Jelszó @@ -422,11 +403,9 @@ Lejátszás erősítése Mindig fogadja el a videó kéréseket Használja az eszköz csengőhangját - Elrejti az állapot és navigációs sávokat Hívásfogadások önműködően Cseng a bejövő korai média közben Üres csevegő szobák elrejtése - Leküldéses értesítések engedélyezése Kettős mód engedélyezve Önműködő Alapértelmezésként használja @@ -451,7 +430,6 @@ A hívás önműködően elindul, ha egy másik alkalmazásból indul Ha a megadott maximális méretnél könnyebb Ha engedélyezve van, a névjegyek parancsikonjai váltják fel - A Flexisip < 2.0 használatakor szükséges Mindig kérdezze meg, hogy melyik fiókba mentse az újonnan létrehozott névjegyet A változásokat a következő indításkor alkalmazzák Kiszolgáló feltöltés URL-jének naplói @@ -460,7 +438,6 @@ Ha beír egy számot, alkalmazza az előhívószámot Biztosan törli ezt az elemet\? Egy SIP-cím mező hozzáadása - Egyesítse az összes hívást egy konferenciába Hang átirányítása a fülhallgatóhoz Információs parancsikonok beszúrása a &appName; névjegyből a natív Android névjegyekbe Akár fájlok letöltéséhez csatolt beérkezett csevegő üzenetek önműködően vagy sem @@ -477,7 +454,6 @@ Ne szerkessze hacsak nem tudja, mit csinál! A bejövő fájlok önműködő letöltésének szabályzata Ha hiányoznak csevegőszobák, próbálja meg törölni ezt a beállítást - Használjon örökölt leküldéses értesítési paramétereket Jelenléti információk natív névjegyben Hozzon létre parancsikonokat a névjegyekhez az indítóban Helyettesíti a csevegőszoba parancsikonjait ha bármilyen @@ -509,12 +485,7 @@ Mutassa meg, hogy mire fogják használni a telefonszámát Ürítse ki a mellette lévő szöveg mezőt Váltás az elülső kameráról hátra és fordítva - Hívását várakoztattuk - Csúsztatás jobbra a hívás befejezéséhez - Csúsztatás balra a hívás fogadásához Hang átirányítása a hangszóróhoz - Folytassa a jelenleg tartott hívást - A számok megjelenítése vagy elrejtése a DTMF-ek küldéséhez Titkosított beszélgetésekbe meghívható partner Összes elem kijelölése a listán 2 @@ -547,7 +518,6 @@ Maximális hívásminőség A hívás biztosítva A hívásbiztonság függőben van - Hívás statisztikák megjelenítése vagy elrejtése Videómelléklet Az értesítési buborék bezárása A beszélgetés megnyitása az alkalmazásban a buborék helyett @@ -570,7 +540,6 @@ &appName; azonnali üzenetek értesítései Szeretné megpróbálni megnyitni egyszerű szöveges fájlként\? Engedélyezze vagy tiltsa le a hívás rögzítését - Folytassa a jelenleg tartott konferenciát Az ismerős egy &appName; felhasználó A kijelölt elemek törlése a listából 9 @@ -578,7 +547,6 @@ Az összes hívás megjelenítése Titkosított A hívás nincs biztosítva - Készítsen képernyőképet a kapott videóról Az elutasított hívások átirányítása a hangposta URI-ra Ugrás a hívás részleteire A lista összes elemének kijelölése megszüntetése @@ -587,14 +555,11 @@ Beszélgetés kiválasztása vagy létrehozása az üzenet továbbításához Beszélgetés kiválasztása vagy létrehozása a fájl(ok) megosztásához Az üzenet továbbításának visszavonása - Ideiglenes üzenetek engedélyezése (béta) Beszélgetés kiválasztása vagy létrehozása a szöveg megosztásához Üzenet továbbítása ebben a beszélgetésben Válasz Üzenet Tartsa lenyomva a gombot a hangüzenet felvételéhez - Az üzenethez mellékelt fájl - Hangüzenet felvétele Hangüzenet felvétele Hangfelvétel leállítása Hangfelvétel szüneteltetése diff --git a/app/src/main/res/values-night-v27/styles.xml b/app/src/main/res/values-night-v27/styles.xml new file mode 100644 index 000000000..1bba957ec --- /dev/null +++ b/app/src/main/res/values-night-v27/styles.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml index 83cce0c6d..6c75a077c 100644 --- a/app/src/main/res/values-night/styles.xml +++ b/app/src/main/res/values-night/styles.xml @@ -1,7 +1,7 @@ - + + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6b2460429..1ff7a8424 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -6,8 +6,6 @@ 传输 UDP 用户数据报协议 TLS安全传输层协议 - MD5哈希散列算法 - SHA-256加密算法 已连接 未连接 正在进行连接 @@ -102,8 +100,6 @@ 安卓通知设置 只使用WiFi 允许IPv6 - 允许推送通知 - 使用旧式推送通知参数 要使用的SIP端口 订阅好友列表 本机联系人中的状态信息 @@ -185,28 +181,18 @@ 返回 清空旁边的文本字段 从前置摄像头切换到后置摄像头,反之亦然 - 从会议中删除参与者 - 暂停会议 - 将所有通话合并到一个会议中 - 您的通话已被置于保留 启用或禁用通话录音 终止通话 - 向右滑动可终止通话 - 向左滑动以接听电话 接听来电 使麦克风静音 将音频传到扬声器 将音频传到耳机 返回通话视图 - 恢复当前保留的通话 - 恢复当前保留的会议 - 显示对话列表 禁用通话视频 启用通话视频 显示或隐藏音频输出菜单 使用蓝牙耳机为音频输出 使用耳机为音频输出 - 显示或隐藏高级操作菜单 发起新通话 将当前通话转接给其他人 待传输文件 @@ -240,7 +226,6 @@ 保存更改 更改联系人图片 更改自己的图像 - 通话方向 前往通话记录 退出编辑模式 选择列表中的所有项目 @@ -297,13 +282,11 @@ 最佳通话质量 通话不安全 安全通话 - 显示或隐藏通话统计 视频附件 来电时振动 自动接听时间 是否自动下载接收到的聊天信息中附加的文件 在启动器中创建至聊天室的快捷方式 - 使用Flexisip <2.0时需要 从&appName;联系人信息快捷方式插入Android本机通讯录 请勿随意编辑! 关联您的帐户 @@ -319,7 +302,6 @@ 显示您的电话号码将用于什么 暂停通话 取消麦克风静音 - 显示或隐藏要发送DTMF的数字 使用扬声器作为音频输出 信息是「阅后即焚」 端到端加密已禁用 @@ -337,7 +319,6 @@ &appName; 联系人 查看我们的隐私策略 日志链接已复制到剪贴板 - 出错 电话号码 电话号码 选择您的国家 @@ -355,7 +336,6 @@ %d 天 共享日志链接使用… - 邮箱无效 今日 退出 通话 @@ -422,8 +402,6 @@ 发送状态 发送状态 添加联系人 - 无法打开文件,没有适用于此格式的应用程序。 - 聊天室创建失败 组信息 对话中的设备 「阅后即焚」信息 @@ -445,11 +423,9 @@ 无录音 来电 去电 - 通话已被对方暂停 暂停通话 去电 通话中 - 会议 通话已被拒绝 用户忙线中 找不到用户 @@ -481,7 +457,6 @@ 密码错误 电子邮件地址无效 用户名过长 - 您的账户还未激活 账户不存在或密码不匹配 使用您的&appName;账户 请确认您的国家码再输入您的电话号码 @@ -494,7 +469,6 @@ 继续 使用SIP账户 显示名(可选) - 哈希算法 请为您的&appName;帐户输入用户名和密码 密码确认 接收方丢失率: @@ -532,15 +506,12 @@ 离开群组 已转发 是否要在此聊天室转发信息? - 选择一个对话或创建一个新的对话 短暂信息 群组创建失败 \@string/chat_room_delete_one_dialog 对方要求启用视讯 - 显示筛选器: - \n益于您的电话号码,您的朋友们可以更容易的找到您。\n\n您在通讯录里可以查看谁在使用&appName;,他们也同样可以通过&appName;联系您。\n \n如果将您的帐户关联您的电话号码,您的朋友们可以更容易的找到你。\n\n您在通讯录里可以查看谁在使用&appName;,他们也同样可以通过&appName;联系您。\n 请输入您的用户名与密码和您的SIP域名 您的帐户已经创建。请检查邮件以驗證您的帐户: @@ -553,7 +524,6 @@ 重启日志 显示名 用户名 - 启用短暂消息(测试版) 上传日志失败 查看配置文件 回复 @@ -571,8 +541,6 @@ DTLS 如果输入了数字,则将前缀应用于数字 留言即回复 - 录制音频信息 - 截取收到的视频截图 在应用程序中打开对话而不是气泡 日志已清除 回复 @@ -597,8 +565,6 @@ 隐私政策 我接受 Belledonne Communications 的 %1$s 和 %2$s 需要一些额外的权限 - 通话时全屏应用 - 隐藏状态栏和导航栏 在应用程序外显示叠加层 音频焦点丢失时暂停通话 自动开始通话录音 @@ -611,7 +577,6 @@ &appName; 未接来电通知 应用前缀于拨出的电话和聊天 在此对话中转发消息 - 附加到消息的文件 关闭通知气泡 在第三方应用程序中打开文件 取消消息转发 @@ -631,4 +596,4 @@ 消息将被删除 中止 我明白 - \ No newline at end of file + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index e1bf7eaa8..c6a3e16a4 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -10,7 +10,6 @@ &appName;聯絡人 訪問我們的隱私政策 日誌網址複製到剪貼板 - 錯誤 電話號碼 電話號碼 前綴 @@ -26,15 +25,12 @@ %d 天 - 郵箱無效 今日 昨日 傳輸 UDP用戶數據報協議 TCP傳輸控制協議 TLS安全傳輸層協議 - MD5 哈希散列算法 - SHA-256加密算法 已連接 未連接 正在進行連接 @@ -79,7 +75,6 @@ %s造成安全級別下降 參加人數上限超過%s人 您禁用了「閱後即焚」訊息 - 您開啟了「閱後即焚」訊息 「閱後即焚」訊息到期日:%s 下載 選擇源 @@ -117,9 +112,6 @@ 發送狀態 刪除 加入通訊錄 - 選擇一個對話或創建一個新的對話 - 無法打開文件,沒有適用於此格式的應用程序。 - 聊天室創建失敗 群組信息 對話中的設備 「閱後即焚」信息 @@ -142,11 +134,9 @@ 無錄音 來電 去電 - 通話已被對方暫停 暫停通話 去電 通話中 - 會議 用戶忙線中 找不到用戶 不兼容的媒體參數 @@ -169,7 +159,6 @@ 編碼器: 解碼器: 播放過濾器: - 顯示過濾器: 捕捉過濾器: 歡迎 創建帳戶 @@ -197,7 +186,6 @@ 使用SIP帳戶 請輸入您的用戶名、密碼和SIP域名 顯示名字(可選) - 哈希算法 確認密碼 您的帳戶已經創建。請檢查郵件以驗證您的帳戶: 一旦完成,請返回並點擊此處。 @@ -211,9 +199,7 @@ URL網址 不明的URL網址格式,無法下載配置資源… 助手將會幫助您配置和使用您的SIP帳戶。 - \n益於您的電話號碼,您的朋友們將更容易找到您。\n\n您在通訊錄裡可以查看誰在使用&appName;,他們也同樣可以通過&appName;聯繫您。\n 您的電話號碼只能鏈接一個&appName;賬戶。\n\n如果您已經把您的號碼鏈接了其他帳戶,但是您更想使用這個。只需鏈接您現在的賬戶,你的號碼就會自動轉移到這個賬戶。 - 您的帳戶尚未經驗證 帳戶不存在或密碼不匹配 繼續 請為您的&appName;帳戶輸入用戶名和密碼 @@ -293,7 +279,6 @@ 重疊來電通知 已開始回声消除器校准 迴聲測試已停止 - 使用舊式推通知參數 訂閱好友列表 在啟動器中創建聯絡人的快捷方式 顯示通知以使應用程序保持活動狀態 @@ -310,9 +295,7 @@ 確認您的對話者說: 安全的對話中的即時短信是被端到端加密的。通過驗證參與者可以提高對話的安全級別。為此,請致電聯繫人並遵循身份驗證過程。 從原始攝像頭切換到後置攝像頭,反之亦然 - 向左滑以接聽來電 取消麥克風靜音 - 恢復當前保留的通話 顯示或隱藏音頻輸出菜單 將當前的通話轉接給別人 端到端加密已禁用 @@ -346,8 +329,6 @@ 安卓通知設置 只使用Wi-Fi 允許IPv6 - 允許推送通知 - 使用Flexisip <2.0時需要 使用隨機端口 要使用的SIP端口 本機聯繫人中的狀態信息 @@ -444,28 +425,19 @@ 返回 顯示您的電話號碼將用於什麼 清空旁邊的文本字段 - 從會議中刪除參與者 - 暫停會議 - 將所有通話合併到一個會議中 - 您的通話已被置於保留 暫停通話 啟用或禁用通話錄音 終止通話 - 向右滑可終止通話 接聽來電 使麥克風靜音 將音頻傳到揚聲器 將音頻傳到耳機 返回通話視圖 - 恢復當前保留的會議 - 顯示或隱藏要發送DTMF的數字 - 顯示對話列表 啟用通話視頻 禁用通話視頻 使用藍芽耳機為音頻輸出 使用耳機為音頻輸出 使用揚聲器為音頻輸出 - 顯示或隱藏高級操作菜單 發起新通話 待傳輸文件 刪除待傳輸文件 @@ -498,7 +470,6 @@ 保存更改 更改聯絡人圖片 更改自己的圖像 - 通話方向 前往通訊錄 退出編輯模式 選擇列表中的所有項目 @@ -540,7 +511,6 @@ 通話不安全 安全通話 通話安全性待定 - 顯示後隱藏通話統計 視頻附件 顯示名 用戶名 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index fc3b14770..cac2445e5 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -13,6 +13,8 @@ + + @@ -23,12 +25,23 @@ - + + + + + + + + + + + + @@ -49,4 +62,23 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 3bbc1bda6..83700c7fa 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -15,11 +15,34 @@ #ffffff #ff0000 #96c11f + #7d9f21 + + #798791 + #D0D8DE + #4B5964 + #96A5B1 + #D8D8D8 + #EBEBEB + #F0F1F2 + #A64B5964 + #E64B5964 + #E4E4E4 + #AFAFAF + #303030 + #A6B2BC + #252E35 + #3F464B + #475663 + #2D3841 + #353B3F + #6E8596 + #A2A2A2 + + #F7F7F7 #3eb5c0 #e1e1e1 #f3f3f3 - #c2c2c2 #a1a1a1 #f3f3f3 diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 9344f15d7..0098f2806 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -8,7 +8,6 @@ 200dp 100dp 120dp - 30dp 50dp 600dp 200dp @@ -19,9 +18,6 @@ 5dp 5dp 35dp - 60dp - 300dp - 240dp 20dp 3dp 60dp @@ -38,4 +34,26 @@ 8sp 14sp 1sp + 50dp + 10dp + 0dp + 60dp + 80dp + 50dp + 250dp + 60dp + 200dp + 210dp + 60dp + 50dp + 260dp + 350dp + 400dp + 390dp + 20dp + 5dp + 100dp + 10dp + 60dp + 50dp \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d1782128e..fca3b40ea 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,7 +31,6 @@ linphone-android@belledonne-communications.com - Error Phone number Phone numbers Select your country @@ -52,7 +51,6 @@ Share logs link using… - Invalid email Today @@ -63,8 +61,6 @@ UDP TCP TLS - MD5 - SHA-256 the libre SIP client @@ -87,6 +83,7 @@ Calls No call in your history No missed call in your history + No conference call in your history Do you want to delete this record? Do you want to delete these records? @@ -133,6 +130,8 @@ @string/debug_popup_show_config_file @string/cancel + Call is being transferred + Can\'t transfer call No conversations @@ -181,13 +180,10 @@ Add to contacts Do you want to forward the message in this room? dummy subject - Select a conversation or create a new one Select or create a conversation to forward the message Select or create a conversation to share the file(s) Select or create a conversation to share the text - Can\'t open file, no application available for this format. File not found - Chat room creation failed Group info Conversation\'s devices Ephemeral messages @@ -229,18 +225,72 @@ Message will be deleted Abort + Failed to create chat room No recordings + + Conference + Start a conference + Do you want to schedule this conference for later? + Mandatory + Subject + Conference subject + Conference address + Add a description + Description + Date + Time + Duration + Timezone + Send invite via &appName; + Send invite via email + Would you like to encrypt the conference? + Invite will be sent out from my &appName; account + Participants list + Conference info + Create conference + Schedule conference + Conference address copied into clipboard + Failed to create conference! + Failed to send conference info to a participant + You are currently out of the conference. + Click on play button to join it back. + Remote conference + Local conference + Conference invite: + Description: + Join + %d participants + Mosaic mode + Active speaker mode + Start + Cancel + Video is currently disabled + Conferences + You can\'t change conference layout as there is too many participants + There is too many participants for mosaic layout, switching to active speaker + (paused) + No scheduled conference yet. + Organizer: + Conference\'s chat room + Failed to create conference + %s is now admin + %s is no longer admin + Conference has been scheduled + Do you really want to delete this conference? + Conference info has been deleted + You are currently alone in this conference + You have been invited to a conference + Conference invitation + Incoming Call Outgoing Call - Call has been paused by your correspondent Paused call Outgoing call Call running - Conference Call has been declined User is busy User hasn\'t been found @@ -251,6 +301,23 @@ Temporarily unavailable Error: %s Correspondent would like to turn the video on + Participants list + Chat + Calls list + Numpad + Change layout + Call statistics + Start new call + Transfer call + Resume call + Pause call + Transfer call + Answer call + Terminate call + This call is being recorded. + Call has been paused by remote. + You have paused the call. + Click on play button to resume it. Audio @@ -271,7 +338,6 @@ Encoder: Decoder: Player filter: - Display filter: Capture filter: @@ -283,7 +349,6 @@ Fetch remote configuration Echo canceler calibration in progress What will my phone number be used for? - \nThanks to your phone number, your friends will find you more easily.\n\n You will see in your address book who is using &appName; and your friends will know that they can reach you on &appName; as well.\n \nYour friends will find you more easily if you link your account to your phone number\n\nYou will see in your address book who is using &appName; and your friends will know that they can reach you on &appName; as well.\n You can only use your phone number with one &appName; account.\n\nIf you had already linked your number to an other account but you prefer to use this one, simply link it now and your number will automatically be moved to this account. https://www.linphone.org/general-terms @@ -303,7 +368,6 @@ Passwords do not match Email address is invalid Username has too many characters - Your account has not been validated yet Account does not exist or password does not match @@ -322,7 +386,6 @@ Use SIP account Please enter your username and password with your SIP domain Display name (optional) - Hash algorithm Please enter a username, email and password for your &appName; account @@ -359,6 +422,7 @@ Network Contacts Advanced + Conferences Primary Account Display Name Username @@ -417,8 +481,6 @@ Media encryption mandatory Improve interactions with bluetooth devices Requires some extra permissions - Full screen app while in call - Hides status and navigation bars Overlay call notification Show overlay outside of app You will be asked to grant overlay permission @@ -438,6 +500,7 @@ Pause calls when audio focus is lost Android notification settings Automatically start call recording + Get notified when call is being recorded by your correspondent Mark as read upon notification dismissal @@ -462,15 +525,11 @@ Android notification settings Always open files inside this app You\'ll still be able to export them in third-party apps - Enable ephemeral messages (beta) Auto download incoming voice recordings Use WiFi only Allow IPv6 - Enable push notifications - Use legacy push notification params - Required when using Flexisip < 2.0 Use random ports SIP port to use @@ -564,6 +623,11 @@ Apply prefix for outgoing calls and chat If a number is entered, apply prefix to number Replace + by 00 + Conference factory URI + Audio/video conference factory URI + + + Default layout linphone_notification_service_id @@ -616,32 +680,21 @@ Show what your phone number will be used for Empty the text field next to it Switch from front camera to back and vice versa - Remove participant from the conference - Pause the conference - Merge all calls into one conference - Your call has been put on hold Pause the call Enable or disable the recording of the call Terminate the call - Slide to the right to terminate the call - Slide to the left to answer the call Answer the call Mute your microphone Un-mute your microphone Route audio to the speaker Route audio to the earpiece Go back to the call view - Resume the call currently on hold - Resume the conference currently on hold - Display or hide the digits to send DTMFs - Display the conversations list Enable video in call Disable video in call Show or hide audio output menu Use bluetooth headset as audio output Use earpiece as audio output Use speaker as audio output - Show or hide advanced actions menu Start a new call Transfer current call to someone else Pending file transfer @@ -664,9 +717,7 @@ Show chat room menu Enter edition mode Attach a file to the message - File attached to the message Send message - Record audio message Show or hide the participant devices Ephemeral duration selected Change ephemeral duration by selected value @@ -685,7 +736,6 @@ Save changes Change contact picture Change own picture - Call direction Go to call details Quit edition mode Select all items in list @@ -731,9 +781,7 @@ Call is not secured Call is secured Call security is pending - Show or hide call statistics Video attachment - Take a screenshot of received video Close notification bubble Open conversation in app instead of bubble Open file in third-party app @@ -746,4 +794,15 @@ Pause voice recording Play voice recording Scroll to bottom or first unread message + Open call context menu + Create a conference call + Copy conference address + Edit conference + Delete conference + Conference will be encrypted + Export event to calendar + Hide call statistics + Show numpad + Hide numpad + Go to conversations list diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 276db38de..5e950e187 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -7,8 +7,7 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + diff --git a/gradle.properties b/gradle.properties index fd519955b..f9cfc7eac 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx4096m +org.gradle.jvmargs=-Xmx8192m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects