From 6d086251687b2f4cd8ef0585cd1dbc2a1093237d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 27 Jan 2025 10:38:00 +0100 Subject: [PATCH 001/593] Fixed group call missed notification & in-call alert titles --- .../notifications/NotificationsManager.kt | 14 ++++++++++---- .../linphone/ui/main/viewmodel/MainViewModel.kt | 15 ++++++--------- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index a39aef01d2..0e3bd47a3b 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -614,10 +614,16 @@ class NotificationsManager Log.i("$TAG Updating missed calls notification count to $missedCallCount") } else { val remoteAddress = call.callLog.remoteAddress - val friend: Friend? = coreContext.contactsManager.findContactByAddress(remoteAddress) - body = context.getString(R.string.notification_missed_call) - .format(friend?.name ?: LinphoneUtils.getDisplayName(remoteAddress)) - Log.i("$TAG Creating missed call notification") + val conferenceInfo = call.callLog.conferenceInfo + body = if (conferenceInfo != null) { + context.getString(R.string.notification_missed_group_call) + .format(conferenceInfo.subject ?: LinphoneUtils.getDisplayName(remoteAddress)) + } else { + val friend: Friend? = coreContext.contactsManager.findContactByAddress(remoteAddress) + context.getString(R.string.notification_missed_call) + .format(friend?.name ?: LinphoneUtils.getDisplayName(remoteAddress)) + } + Log.i("$TAG Creating missed call notification with title [$body]") } val pendingIntent = NavDeepLinkBuilder(context) diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt index da7391924a..b107cd8fb7 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt @@ -472,17 +472,14 @@ class MainViewModel val currentCall = core.currentCall ?: core.calls.firstOrNull() if (currentCall != null) { val address = currentCall.callLog.remoteAddress - val contact = coreContext.contactsManager.findContactByAddress(address) - val label = if (contact != null) { - contact.name ?: LinphoneUtils.getDisplayName(address) + val conferenceInfo = LinphoneUtils.getConferenceInfoIfAny(currentCall) + val label = if (conferenceInfo != null) { + conferenceInfo.subject ?: LinphoneUtils.getDisplayName(address) } else { - val conferenceInfo = coreContext.core.findConferenceInformationFromUri( - address - ) - conferenceInfo?.subject ?: LinphoneUtils.getDisplayName( - address - ) + val contact = coreContext.contactsManager.findContactByAddress(address) + contact?.name ?: LinphoneUtils.getDisplayName(address) } + Log.i("$TAG Showing single call alert with label [$label]") addAlert(SINGLE_CALL, label) callsStatus.postValue(LinphoneUtils.callStateToString(currentCall.state)) } diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index fc1f5669c4..c88fb10dc4 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -54,6 +54,7 @@ Marquer comme lu Répondre Appel manqué de %s + Appel de groupe manqué : %s %s appels manqués Appel manqué &appName; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d68214cf86..f23abeceaa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -92,6 +92,7 @@ Mark as read Reply Missed call from %s + Missed group call: %s %s missed calls Missed call &appName; From 0bf50f1495c4758e957cec765cab6da5e7f19dd7 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 11 Mar 2025 16:23:32 +0100 Subject: [PATCH 002/593] Fixed wrong constraint layout reference --- app/src/main/res/layout-sw600dp/assistant_landing_fragment.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout-sw600dp/assistant_landing_fragment.xml b/app/src/main/res/layout-sw600dp/assistant_landing_fragment.xml index 9021d47a63..c9062028d2 100644 --- a/app/src/main/res/layout-sw600dp/assistant_landing_fragment.xml +++ b/app/src/main/res/layout-sw600dp/assistant_landing_fragment.xml @@ -45,7 +45,7 @@ android:src="@drawable/assistant_logo" app:layout_constraintStart_toEndOf="@id/title" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintBottom_toTopOf="@id/header" + app:layout_constraintBottom_toTopOf="@id/mountains" app:layout_constraintTop_toBottomOf="@id/register" /> Date: Tue, 11 Mar 2025 16:28:51 +0100 Subject: [PATCH 003/593] Updated linphone version to use 5.5.0-alpha --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d5530bb773..130853c9bf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -29,7 +29,7 @@ coil = "3.1.0" dotsIndicator = "5.1.0" photoview = "2.3.0" openidAppauth = "0.11.1" -linphone = "5.4.+" +linphone = "5.5.+" [libraries] androidx-annotations = { group = "androidx.annotation", name = "annotation", version.ref = "annotations" } From 0e6d91a46783a3b608c103d977b766fe82bed3dc Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 12 Mar 2025 10:57:01 +0100 Subject: [PATCH 004/593] Fixed selecting participant in group conversation when typing '@' --- .../chat/model/MessageBottomSheetParticipantModel.kt | 9 ++------- .../linphone/ui/main/chat/model/ParticipantModel.kt | 11 +++-------- .../layout/chat_message_bottom_sheet_list_cell.xml | 2 -- .../main/res/layout/chat_participant_list_cell.xml | 2 -- 4 files changed, 5 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageBottomSheetParticipantModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageBottomSheetParticipantModel.kt index e8e8e158ef..c1c7f69eaa 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageBottomSheetParticipantModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageBottomSheetParticipantModel.kt @@ -46,16 +46,11 @@ class MessageBottomSheetParticipantModel } @UiThread - fun toggleShowSipUri() { + fun clicked() { if (!isOurOwnReaction && !corePreferences.onlyDisplaySipUriUsername) { showSipUri.postValue(showSipUri.value == false) } else { - clicked() + onClick?.invoke() } } - - @UiThread - fun clicked() { - onClick?.invoke() - } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ParticipantModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ParticipantModel.kt index dad8d5b907..c7f171ae61 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ParticipantModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ParticipantModel.kt @@ -58,19 +58,14 @@ class ParticipantModel } @UiThread - fun toggleShowSipUri() { - if (!corePreferences.onlyDisplaySipUriUsername) { + fun onClicked() { + if (onClicked == null && !corePreferences.onlyDisplaySipUriUsername) { showSipUri.postValue(showSipUri.value == false) } else { - onClicked() + onClicked?.invoke(this) } } - @UiThread - fun onClicked() { - onClicked?.invoke(this) - } - @UiThread fun openMenu(view: View) { onMenuClicked?.invoke(view, this) diff --git a/app/src/main/res/layout/chat_message_bottom_sheet_list_cell.xml b/app/src/main/res/layout/chat_message_bottom_sheet_list_cell.xml index 2ddf29f031..d599b59154 100644 --- a/app/src/main/res/layout/chat_message_bottom_sheet_list_cell.xml +++ b/app/src/main/res/layout/chat_message_bottom_sheet_list_cell.xml @@ -32,7 +32,6 @@ Date: Wed, 12 Mar 2025 13:31:40 +0100 Subject: [PATCH 005/593] Prevent user from connecting the same account multiple times --- .../ui/assistant/viewmodel/AccountLoginViewModel.kt | 10 ++++++++++ .../viewmodel/ThirdPartySipAccountLoginViewModel.kt | 10 ++++++++++ app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 22 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountLoginViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountLoginViewModel.kt index 67b93e15b5..e46f441423 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountLoginViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountLoginViewModel.kt @@ -181,6 +181,16 @@ open class AccountLoginViewModel return@postOnCoreThread } + val accounts = core.accountList + val found = accounts.find { + it.params.identityAddress?.weakEqual(identityAddress) == true + } + if (found != null) { + Log.w("$TAG An account with the same identity address [${identityAddress.asStringUriOnly()}] already exists, do not add it again!") + showRedToast(R.string.assistant_account_login_already_connected_error, R.drawable.warning_circle) + return@postOnCoreThread + } + val user = identityAddress.username if (user == null) { Log.e( diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt index e1676881f5..ac2c57db81 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt @@ -203,6 +203,16 @@ class ThirdPartySipAccountLoginViewModel return@postOnCoreThread } + val accounts = core.accountList + val found = accounts.find { + it.params.identityAddress?.weakEqual(identityAddress) == true + } + if (found != null) { + Log.w("$TAG An account with the same identity address [${identityAddress.asStringUriOnly()}] already exists, do not add it again!") + showRedToast(R.string.assistant_account_login_already_connected_error, R.drawable.warning_circle) + return@postOnCoreThread + } + newlyCreatedAuthInfo = Factory.instance().createAuthInfo( user, userId, diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0693326d71..f0724223ec 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -106,6 +106,7 @@ Single sign on Adresse SIP invalide L\'adresse SIP ne contient pas de nom d\'utilisateur ! + Le compte est déjà connecté Pas encore de compte ? Créer un compte Confirmez votre numéro diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cfc3398f7d..a4087876df 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -145,6 +145,7 @@ Single sign on SIP address is invalid! SIP address doesn\'t contains a username! + Account already exists No account yet? Register Confirm your phone number From cc5bfcf14d12325029decf6bccccbb27b082223a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 13 Mar 2025 09:45:19 +0100 Subject: [PATCH 006/593] Use Account's onConferenceInformationUpdated callback to refresh meetings list --- .../viewmodel/MeetingsListViewModel.kt | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt index c3a2fb6c2d..d9dfdd8ce5 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt @@ -23,6 +23,8 @@ import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Account +import org.linphone.core.AccountListenerStub import org.linphone.core.Address import org.linphone.core.ConferenceInfo import org.linphone.core.ConferenceScheduler @@ -58,7 +60,20 @@ class MeetingsListViewModel @WorkerThread override fun onConferenceInfoReceived(core: Core, conferenceInfo: ConferenceInfo) { Log.i("$TAG Conference info received [${conferenceInfo.uri?.asStringUriOnly()}]") - computeMeetingsList(currentFilter) + computeMeetingsListFromLocallyStoredInfo() + } + } + + private val accountListener = object : AccountListenerStub() { + @WorkerThread + override fun onConferenceInformationUpdated( + account: Account, + infos: Array + ) { + Log.i( + "$TAG Conference information updated with [${infos.size}] items for current account, reloading list" + ) + computeMeetingsList(currentFilter, infos) } } @@ -111,8 +126,9 @@ class MeetingsListViewModel coreContext.postOnCoreThread { core -> core.addListener(coreListener) + core.defaultAccount?.addListener(accountListener) - computeMeetingsList(currentFilter) + computeMeetingsListFromLocallyStoredInfo() } } @@ -121,6 +137,7 @@ class MeetingsListViewModel super.onCleared() coreContext.postOnCoreThread { core -> + core.defaultAccount?.removeListener(accountListener) core.removeListener(coreListener) } } @@ -128,7 +145,7 @@ class MeetingsListViewModel @UiThread override fun filter() { coreContext.postOnCoreThread { - computeMeetingsList(currentFilter) + computeMeetingsListFromLocallyStoredInfo() } } @@ -146,13 +163,7 @@ class MeetingsListViewModel } @WorkerThread - private fun computeMeetingsList(filter: String) { - if (meetings.value.orEmpty().isEmpty()) { - fetchInProgress.postValue(true) - } - - val list = arrayListOf() - + private fun computeMeetingsListFromLocallyStoredInfo() { var source = coreContext.core.defaultAccount?.conferenceInformationList if (source == null) { Log.e( @@ -160,7 +171,16 @@ class MeetingsListViewModel ) source = coreContext.core.conferenceInformationList } + computeMeetingsList(currentFilter, source) + } + @WorkerThread + private fun computeMeetingsList(filter: String, source: Array) { + if (meetings.value.orEmpty().isEmpty()) { + fetchInProgress.postValue(true) + } + + val list = arrayListOf() var previousModel: MeetingModel? = null var previousModelWeekLabel = "" var meetingForTodayFound = false From 11795cded85ea6442acc6f8d3e7c9b2219188691 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 13 Mar 2025 09:56:27 +0100 Subject: [PATCH 007/593] Prevent SecurityException when accessing bluetooth_name on some devices --- .../main/java/org/linphone/utils/AndroidUtils.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/linphone/utils/AndroidUtils.kt b/app/src/main/java/org/linphone/utils/AndroidUtils.kt index 25b1339213..6849c0a1ff 100644 --- a/app/src/main/java/org/linphone/utils/AndroidUtils.kt +++ b/app/src/main/java/org/linphone/utils/AndroidUtils.kt @@ -160,13 +160,19 @@ class AppUtils { context.contentResolver, Settings.Global.DEVICE_NAME ) + if (name == null) { Log.w("$TAG Failed to obtain device name, trying to get bluetooth name") - name = Settings.Secure.getString( - context.contentResolver, - "bluetooth_name" - ) + try { + name = Settings.Secure.getString( + context.contentResolver, + "bluetooth_name" + ) + } catch (e: SecurityException) { + Log.e("$TAG Failed to get bluetooth_name: $e") + } } + if (name == null) { Log.w("$TAG Failed to obtain bluetooth name, using device's manufacturer & model") name = "${Build.MANUFACTURER} ${Build.MODEL}" From 0b6805a73cc7f2553df6564ecc86500c59e4a575 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 13 Mar 2025 10:08:08 +0100 Subject: [PATCH 008/593] Fixed color selector used when not needed, may cause crash on old devices --- app/src/main/res/color/main2_000.xml | 4 ---- app/src/main/res/layout/call_transfer_fragment.xml | 2 +- app/src/main/res/layout/calls_list_fragment.xml | 2 +- app/src/main/res/layout/chat_bubble_content_grid_cell.xml | 2 +- .../res/layout/chat_conversation_attachments_area_cell.xml | 2 +- 5 files changed, 4 insertions(+), 8 deletions(-) delete mode 100644 app/src/main/res/color/main2_000.xml diff --git a/app/src/main/res/color/main2_000.xml b/app/src/main/res/color/main2_000.xml deleted file mode 100644 index b7ada56e21..0000000000 --- a/app/src/main/res/color/main2_000.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/layout/call_transfer_fragment.xml b/app/src/main/res/layout/call_transfer_fragment.xml index b642fa1e3b..4e48c82b1c 100644 --- a/app/src/main/res/layout/call_transfer_fragment.xml +++ b/app/src/main/res/layout/call_transfer_fragment.xml @@ -61,7 +61,7 @@ android:id="@+id/background" android:layout_width="0dp" android:layout_height="0dp" - android:background="@color/main2_000" + android:background="?attr/color_main2_000" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/calls_list_fragment.xml b/app/src/main/res/layout/calls_list_fragment.xml index 10a0cd0b4f..88bcc5d138 100644 --- a/app/src/main/res/layout/calls_list_fragment.xml +++ b/app/src/main/res/layout/calls_list_fragment.xml @@ -65,7 +65,7 @@ android:id="@+id/background" android:layout_width="0dp" android:layout_height="0dp" - android:background="@color/main2_000" + android:background="?attr/color_main2_000" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/chat_bubble_content_grid_cell.xml b/app/src/main/res/layout/chat_bubble_content_grid_cell.xml index 0271da5af1..6ab6fdfee3 100644 --- a/app/src/main/res/layout/chat_bubble_content_grid_cell.xml +++ b/app/src/main/res/layout/chat_bubble_content_grid_cell.xml @@ -125,7 +125,7 @@ android:layout_height="wrap_content" android:layout_marginStart="5dp" android:text="@{model.audioVideoDuration, default=`00:42`}" - android:textColor="@color/main2_600" + android:textColor="?attr/color_main2_600" android:textSize="12sp" android:visibility="@{model.isAudio && model.audioVideoDuration.length() > 0 ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintBottom_toBottomOf="@id/left_background" diff --git a/app/src/main/res/layout/chat_conversation_attachments_area_cell.xml b/app/src/main/res/layout/chat_conversation_attachments_area_cell.xml index 85a07535d6..65589c8f22 100644 --- a/app/src/main/res/layout/chat_conversation_attachments_area_cell.xml +++ b/app/src/main/res/layout/chat_conversation_attachments_area_cell.xml @@ -100,7 +100,7 @@ android:layout_height="wrap_content" android:layout_marginStart="5dp" android:text="@{model.audioVideoDuration, default=`00:42`}" - android:textColor="@color/main2_600" + android:textColor="?attr/color_main2_600" android:textSize="12sp" android:visibility="@{model.isAudio && model.audioVideoDuration.length() > 0 ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintBottom_toBottomOf="@id/file_name" From dee684b364a04557efd3fce37d1ada64b7d3f808 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 13 Mar 2025 11:44:58 +0100 Subject: [PATCH 009/593] Added setting to choose whether to sort contacts by first or last name --- .../java/org/linphone/core/CorePreferences.kt | 7 +++ .../contacts/adapter/ContactsListAdapter.kt | 4 +- .../contacts/fragment/ContactsListFragment.kt | 15 +++--- .../main/contacts/model/ContactAvatarModel.kt | 19 ++++++- .../viewmodel/ContactsListViewModel.kt | 10 ++-- .../settings/fragment/SettingsFragment.kt | 31 ++++++++++++ .../settings/viewmodel/SettingsViewModel.kt | 16 ++++++ .../ui/main/viewmodel/SharedMainViewModel.kt | 4 ++ app/src/main/res/layout/settings_contacts.xml | 49 ++++++++++++++++++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 11 files changed, 144 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 3531293770..269fc1949d 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -167,6 +167,13 @@ class CorePreferences // Contacts related + @get:WorkerThread @set:WorkerThread + var sortContactsByFirstName: Boolean + get() = config.getBool("ui", "sort_contacts_by_first_name", true) // If disabled, last name will be used + set(value) { + config.setBool("ui", "sort_contacts_by_first_name", value) + } + @get:WorkerThread @set:WorkerThread var contactsFilter: String get() = config.getString("ui", "contacts_filter", "")!! // Default value must be empty! diff --git a/app/src/main/java/org/linphone/ui/main/contacts/adapter/ContactsListAdapter.kt b/app/src/main/java/org/linphone/ui/main/contacts/adapter/ContactsListAdapter.kt index 28004dc027..b3572b43c2 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/adapter/ContactsListAdapter.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/adapter/ContactsListAdapter.kt @@ -125,12 +125,12 @@ class ContactsListAdapter( val previousItem = bindingAdapterPosition - 1 val previousLetter = if (previousItem >= 0) { - getItem(previousItem).contactName?.get(0).toString() + getItem(previousItem).sortingName?.get(0).toString() } else { "" } - val currentLetter = contactModel.contactName?.get(0).toString() + val currentLetter = contactModel.sortingName?.get(0).toString() val displayLetter = previousLetter.isEmpty() || currentLetter != previousLetter firstContactStartingByThatLetter = displayLetter diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt index 114259e31f..8a372b1bb9 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt @@ -37,6 +37,7 @@ import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.observe import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager @@ -181,9 +182,7 @@ class ContactsListFragment : AbstractMainFragment() { showFilterPopupMenu(binding.topBar.extraAction) } - sharedViewModel.showContactEvent.observe( - viewLifecycleOwner - ) { + sharedViewModel.showContactEvent.observe(viewLifecycleOwner) { it.consume { refKey -> Log.i("$TAG Displaying contact with ref key [$refKey]") val navController = binding.contactsNavContainer.findNavController() @@ -194,9 +193,7 @@ class ContactsListFragment : AbstractMainFragment() { } } - sharedViewModel.showNewContactEvent.observe( - viewLifecycleOwner - ) { + sharedViewModel.showNewContactEvent.observe(viewLifecycleOwner) { it.consume { if (findNavController().currentDestination?.id == R.id.contactsListFragment) { Log.i("$TAG Opening contact editor for creating new contact") @@ -207,6 +204,12 @@ class ContactsListFragment : AbstractMainFragment() { } } + sharedViewModel.forceRefreshContactsList.observe(viewLifecycleOwner) { + it.consume { + listViewModel.filter() + } + } + // AbstractMainFragment related listViewModel.title.value = getString(R.string.bottom_navigation_contacts_label) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt index f835624e07..6e85704400 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt @@ -36,6 +36,7 @@ import org.linphone.core.tools.Log import org.linphone.utils.AppUtils import org.linphone.utils.TimestampUtils import androidx.core.net.toUri +import org.linphone.LinphoneApplication.Companion.corePreferences class ContactAvatarModel @WorkerThread @@ -56,7 +57,9 @@ class ContactAvatarModel val name = MutableLiveData() - val firstLetter: String = AppUtils.getFirstLetter(friend.name.orEmpty()) + var sortingName: String? = null + + var firstLetter: String? = null private val friendListener = object : FriendListenerStub() { @WorkerThread @@ -76,6 +79,7 @@ class ContactAvatarModel } update(address) + refreshSortingName() } @WorkerThread @@ -85,6 +89,12 @@ class ContactAvatarModel } } + @WorkerThread + fun refreshSortingName() { + sortingName = getNameToUseForSorting() + firstLetter = AppUtils.getFirstLetter(getNameToUseForSorting().orEmpty()) + } + @WorkerThread fun update(address: Address?) { updateSecurityLevel(address) @@ -148,6 +158,13 @@ class ContactAvatarModel } } + @WorkerThread + fun getNameToUseForSorting(): String? { + val sortByFirstName = corePreferences.sortContactsByFirstName + val firstOrLastName = if (sortByFirstName) friend.vcard?.givenName else friend.vcard?.familyName + return firstOrLastName ?: friend.name ?: friend.organization ?: friend.vcard?.fullName + } + @WorkerThread private fun getAvatarUri(friend: Friend): Uri? { val picturePath = friend.photo diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt index 13b28858fa..efddee3ce1 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt @@ -284,6 +284,7 @@ class ContactsListViewModel val list = arrayListOf() val favouritesList = arrayListOf() var count = 0 + val collator = Collator.getInstance(Locale.getDefault()) for (result in results) { val friend = result.friend @@ -308,6 +309,7 @@ class ContactsListViewModel } else { coreContext.contactsManager.getContactAvatarModelForAddress(result.address) } + model.refreshSortingName() list.add(model) count += 1 @@ -319,16 +321,18 @@ class ContactsListViewModel } if (firstLoad && count == 20) { + list.sortWith { model1, model2 -> + collator.compare(model1.getNameToUseForSorting(), model2.getNameToUseForSorting()) + } contactsList.postValue(list) } } - val collator = Collator.getInstance(Locale.getDefault()) favouritesList.sortWith { model1, model2 -> - collator.compare(model1.friend.name, model2.friend.name) + collator.compare(model1.getNameToUseForSorting(), model2.getNameToUseForSorting()) } list.sortWith { model1, model2 -> - collator.compare(model1.friend.name, model2.friend.name) + collator.compare(model1.getNameToUseForSorting(), model2.getNameToUseForSorting()) } favourites.postValue(favouritesList) diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt index 1f58890f70..0bb802a722 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt @@ -38,6 +38,7 @@ import org.linphone.ui.main.fragment.GenericMainFragment import org.linphone.utils.ConfirmationDialogModel import org.linphone.ui.main.settings.viewmodel.SettingsViewModel import org.linphone.utils.DialogUtils +import org.linphone.utils.Event @UiThread class SettingsFragment : GenericMainFragment() { @@ -49,6 +50,20 @@ class SettingsFragment : GenericMainFragment() { private lateinit var viewModel: SettingsViewModel + private val sortContactsByListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + val label = viewModel.sortContactsByNames[position] + val value = viewModel.sortContactsByValues[position] + Log.i("$TAG Selected contact sorting is now [$label] ($value)") + viewModel.setContactSorting(value) + + sharedViewModel.forceRefreshContactsList.postValue(Event(true)) + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + } + } + private val layoutListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { val label = viewModel.availableLayoutsNames[position] @@ -155,6 +170,22 @@ class SettingsFragment : GenericMainFragment() { } } + // Setup sort contacts by spinner + val sortContactsByAdapter = ArrayAdapter( + requireContext(), + R.layout.drop_down_item, + viewModel.sortContactsByNames + ) + sortContactsByAdapter.setDropDownViewResource(R.layout.generic_dropdown_cell) + binding.contactsSettings.sortContactsByFirstNameSpinner.adapter = sortContactsByAdapter + + viewModel.sortContactsBy.observe(viewLifecycleOwner) { sort -> + binding.contactsSettings.sortContactsByFirstNameSpinner.setSelection( + viewModel.sortContactsByValues.indexOf(sort) + ) + } + binding.contactsSettings.sortContactsByFirstNameSpinner.onItemSelectedListener = sortContactsByListener + viewModel.addLdapServerEvent.observe(viewLifecycleOwner) { it.consume { if (findNavController().currentDestination?.id == R.id.settingsFragment) { diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 62a36d08bd..3110077cc4 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -97,6 +97,13 @@ class SettingsViewModel // Contacts settings val showContactsSettings = MutableLiveData() + val sortContactsBy = MutableLiveData() + val sortContactsByNames = arrayListOf( + AppUtils.getString(R.string.contact_editor_first_name), + AppUtils.getString(R.string.contact_editor_last_name), + ) + val sortContactsByValues = arrayListOf(0, 1) + val ldapAvailable = MutableLiveData() val ldapServers = MutableLiveData>() @@ -293,6 +300,8 @@ class SettingsViewModel corePreferences.markConversationAsReadWhenDismissingMessageNotification ) + sortContactsBy.postValue(if (corePreferences.sortContactsByFirstName) 0 else 1) + defaultLayout.postValue(core.defaultConferenceLayout.toInt()) theme.postValue(corePreferences.darkMode) @@ -467,6 +476,13 @@ class SettingsViewModel expandContacts.value = expandContacts.value == false } + @UiThread + fun setContactSorting(sortingValue: Int) { + coreContext.postOnCoreThread { core -> + corePreferences.sortContactsByFirstName = sortingValue == 0 + } + } + @UiThread fun addLdapServer() { addLdapServerEvent.value = Event(true) diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt index 10643b3053..2ce556ef59 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt @@ -95,6 +95,10 @@ class SharedMainViewModel MutableLiveData>() } + val forceRefreshContactsList: MutableLiveData> by lazy { + MutableLiveData>() + } + var sipAddressToAddToNewContact: String = "" // Call logs related diff --git a/app/src/main/res/layout/settings_contacts.xml b/app/src/main/res/layout/settings_contacts.xml index 733ca61b9b..40d189ed4c 100644 --- a/app/src/main/res/layout/settings_contacts.xml +++ b/app/src/main/res/layout/settings_contacts.xml @@ -15,6 +15,53 @@ android:paddingBottom="20dp" android:background="@drawable/shape_squircle_white_background"> + + + + + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f0724223ec..cc132a3bde 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -205,6 +205,7 @@ Rendre visible dans la galerie les médias téléchargés Marquer la conversation comme lue lorsqu\'une notification de message est supprimée Contacts + Trier les contacts par Ajouter un serveur LDAP Editer le serveur LDAP Ajouter un carnet d\'adresse CardDAV diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a4087876df..2109e1fff5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -244,6 +244,7 @@ Make downloaded media public Mark conversation as read when dismissing message notification Contacts + Sort contacts by Add LDAP server Edit LDAP server Add CardDAV address book From 71b1cf8e7ae4a213fa4dc905f96b41d51e5e6762 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 13 Mar 2025 12:17:19 +0100 Subject: [PATCH 010/593] Make sure Qr Code fragment doesn't use Static Picture camera device --- .../ui/assistant/viewmodel/QrCodeViewModel.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt index f5a0da65d8..9e33efeda8 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt @@ -109,11 +109,15 @@ class QrCodeViewModel } } - val first = core.videoDevicesList.firstOrNull() - if (first != null) { - Log.w("$TAG No back facing camera found, using first one available [$first]") - coreContext.core.videoDevice = first + for (camera in core.videoDevicesList) { + if (camera != "StaticImage: Static picture") { + Log.w("$TAG No back facing camera found, using first one available [$camera]") + coreContext.core.videoDevice = camera + return@postOnCoreThread + } } + + Log.e("$TAG No camera device found!") } } } From 71e1734ca08ca7d38bbb08b219e3c47d353b0929 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 13 Mar 2025 15:28:11 +0100 Subject: [PATCH 011/593] Fixed crash due to currentCall not being initialized --- .../ui/call/viewmodel/CurrentCallViewModel.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 9428233ad8..4677606c22 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -701,6 +701,10 @@ class CurrentCallViewModel @UiThread fun changeAudioOutputDevice() { val routeAudioToSpeaker = isSpeakerEnabled.value != true + if (!::currentCall.isInitialized) { + Log.w("$TAG Current call not initialized yet, do not attempt to change output audio device") + return + } coreContext.postOnCoreThread { core -> var earpieceFound = false @@ -775,12 +779,10 @@ class CurrentCallViewModel Log.i( "$TAG Found less than two devices, simply switching between earpiece & speaker" ) - if (::currentCall.isInitialized) { - if (routeAudioToSpeaker) { - AudioUtils.routeAudioToSpeaker(currentCall) - } else { - AudioUtils.routeAudioToEarpiece(currentCall) - } + if (routeAudioToSpeaker) { + AudioUtils.routeAudioToSpeaker(currentCall) + } else { + AudioUtils.routeAudioToEarpiece(currentCall) } } } From 614ac7f9cf642572310dc6d49dbb68c2aad04b98 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Sun, 16 Mar 2025 20:34:29 +0100 Subject: [PATCH 012/593] Prevent crash if DMTF setting doesn't exists (Samsung A51) --- .../java/org/linphone/core/CoreContext.kt | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index ca69874cbd..70254af20b 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -30,6 +30,7 @@ import android.os.Handler import android.os.HandlerThread import android.os.Looper import android.provider.Settings +import android.provider.Settings.SettingNotFoundException import androidx.annotation.AnyThread import androidx.annotation.UiThread import androidx.annotation.WorkerThread @@ -896,14 +897,18 @@ class CoreContext @WorkerThread fun playDtmf(character: Char, duration: Int = 200, ignoreSystemPolicy: Boolean = false) { - if (ignoreSystemPolicy || Settings.System.getInt( - context.contentResolver, - Settings.System.DTMF_TONE_WHEN_DIALING - ) != 0 - ) { - core.playDtmf(character, duration) - } else { - Log.w("$TAG Numpad DTMF tones are disabled in system settings, not playing them") + try { + if (ignoreSystemPolicy || Settings.System.getInt( + context.contentResolver, + Settings.System.DTMF_TONE_WHEN_DIALING + ) != 0 + ) { + core.playDtmf(character, duration) + } else { + Log.w("$TAG Numpad DTMF tones are disabled in system settings, not playing them") + } + } catch (snfe: SettingNotFoundException) { + Log.e("$TAG DTMF_TONE_WHEN_DIALING system setting not found: $snfe") } } From 87b6c2deef92af194750f60c9b05a95fba0814f4 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Sun, 16 Mar 2025 20:39:06 +0100 Subject: [PATCH 013/593] Prevent crash clinking on link if no browser is installed on device --- .../ui/assistant/fragment/LandingFragment.kt | 13 +++++++++++++ .../ui/assistant/fragment/RegisterFragment.kt | 5 +++++ .../ThirdPartySipAccountWarningFragment.kt | 5 +++++ .../ui/main/fragment/DrawerMenuFragment.kt | 5 +++++ .../ui/main/help/fragment/HelpFragment.kt | 17 +++++++++++++++++ 5 files changed, 45 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt index f613418687..017875fea7 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt @@ -19,6 +19,7 @@ */ package org.linphone.ui.assistant.fragment +import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.os.Bundle @@ -110,6 +111,10 @@ class LandingFragment : GenericFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" + ) } } @@ -213,6 +218,10 @@ class LandingFragment : GenericFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" + ) } } } @@ -227,6 +236,10 @@ class LandingFragment : GenericFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" + ) } } } diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt index 10f0aba41d..693bf485d3 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt @@ -19,6 +19,7 @@ */ package org.linphone.ui.assistant.fragment +import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.os.Bundle @@ -108,6 +109,10 @@ class RegisterFragment : GenericFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" + ) } } diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountWarningFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountWarningFragment.kt index 57e7528be2..050caff265 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountWarningFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountWarningFragment.kt @@ -19,6 +19,7 @@ */ package org.linphone.ui.assistant.fragment +import android.content.ActivityNotFoundException import android.content.Intent import android.os.Bundle import android.view.LayoutInflater @@ -67,6 +68,10 @@ class ThirdPartySipAccountWarningFragment : GenericFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" + ) } } diff --git a/app/src/main/java/org/linphone/ui/main/fragment/DrawerMenuFragment.kt b/app/src/main/java/org/linphone/ui/main/fragment/DrawerMenuFragment.kt index 067a35ea08..95fccd147a 100644 --- a/app/src/main/java/org/linphone/ui/main/fragment/DrawerMenuFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/fragment/DrawerMenuFragment.kt @@ -19,6 +19,7 @@ */ package org.linphone.ui.main.fragment +import android.content.ActivityNotFoundException import android.content.Intent import android.os.Bundle import android.view.Gravity @@ -152,6 +153,10 @@ class DrawerMenuFragment : GenericMainFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$link], IllegalStateException: $ise" ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$link], ActivityNotFoundException: $anfe" + ) } } } diff --git a/app/src/main/java/org/linphone/ui/main/help/fragment/HelpFragment.kt b/app/src/main/java/org/linphone/ui/main/help/fragment/HelpFragment.kt index 8111b60e7a..3d7e7c43ba 100644 --- a/app/src/main/java/org/linphone/ui/main/help/fragment/HelpFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/help/fragment/HelpFragment.kt @@ -19,6 +19,7 @@ */ package org.linphone.ui.main.help.fragment +import android.content.ActivityNotFoundException import android.content.Intent import android.os.Bundle import android.view.LayoutInflater @@ -84,6 +85,10 @@ class HelpFragment : GenericMainFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" + ) } } @@ -96,6 +101,10 @@ class HelpFragment : GenericMainFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" + ) } } @@ -108,6 +117,10 @@ class HelpFragment : GenericMainFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" + ) } } @@ -163,6 +176,10 @@ class HelpFragment : GenericMainFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" + ) } dialog.dismiss() } From dc4619a7d713796636bfedea88ecbc01060edf92 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Sun, 16 Mar 2025 20:43:42 +0100 Subject: [PATCH 014/593] Prevent use of recording ViewModel property not initialized yet --- .../main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt index f070e668bc..4d4bfa87b6 100644 --- a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt @@ -102,6 +102,8 @@ class RecordingMediaPlayerViewModel @WorkerThread private fun initPlayer() { + if (!::recordingModel.isInitialized) return + Log.i("$TAG Creating player") val playbackSoundCard = AudioUtils.getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage() val recordingPlayer = coreContext.core.createLocalPlayer( @@ -158,6 +160,7 @@ class RecordingMediaPlayerViewModel @WorkerThread private fun startPlayback() { if (!::player.isInitialized) return + if (!::recordingModel.isInitialized) return Log.i("$TAG Starting player") if (player.state == Player.State.Closed) { From 3045378eb02fc76e94f635759e609effbcdbeb3a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Sun, 16 Mar 2025 20:47:48 +0100 Subject: [PATCH 015/593] Prevent crash if fails to go to outside activity because it doesn't exists or it isn't found --- .../java/org/linphone/ui/GenericActivity.kt | 22 ++++++++++++------- .../main/contacts/fragment/ContactFragment.kt | 13 +++++++---- .../settings/fragment/SettingsFragment.kt | 19 ++++++++++------ 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/GenericActivity.kt b/app/src/main/java/org/linphone/ui/GenericActivity.kt index 8d5930966c..3471c2c282 100644 --- a/app/src/main/java/org/linphone/ui/GenericActivity.kt +++ b/app/src/main/java/org/linphone/ui/GenericActivity.kt @@ -20,6 +20,7 @@ package org.linphone.ui import android.annotation.SuppressLint +import android.content.ActivityNotFoundException import android.content.Intent import android.content.res.Configuration import android.content.res.Resources @@ -40,6 +41,7 @@ import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.compatibility.Compatibility import org.linphone.core.tools.Log +import org.linphone.ui.main.settings.fragment.SettingsFragment import org.linphone.utils.ToastUtils import org.linphone.utils.slideInToastFromTop import org.linphone.utils.slideInToastFromTopForDuration @@ -224,15 +226,19 @@ open class GenericActivity : AppCompatActivity() { fun goToAndroidPermissionSettings() { Log.i("$TAG Going into Android settings for our app") - val intent = Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.fromParts( - "package", - packageName, null + try { + val intent = Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts( + "package", + packageName, null + ) ) - ) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + } catch (anfe: ActivityNotFoundException) { + Log.e("$TAG Failed to go to android settings: $anfe") + } } protected fun enableWindowSecureMode(enable: Boolean) { diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt index 7b6901abc0..40950ee1e4 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt @@ -20,6 +20,7 @@ package org.linphone.ui.main.contacts.fragment import android.app.Dialog +import android.content.ActivityNotFoundException import android.content.ClipData import android.content.ClipboardManager import android.content.Context @@ -170,11 +171,15 @@ class ContactFragment : SlidingPaneChildFragment() { viewModel.openNativeContactEditor.observe(viewLifecycleOwner) { it.consume { uri -> - val editIntent = Intent(Intent.ACTION_EDIT).apply { - setDataAndType(uri.toUri(), ContactsContract.Contacts.CONTENT_ITEM_TYPE) - putExtra("finishActivityOnSaveCompleted", true) + try { + val editIntent = Intent(Intent.ACTION_EDIT).apply { + setDataAndType(uri.toUri(), ContactsContract.Contacts.CONTENT_ITEM_TYPE) + putExtra("finishActivityOnSaveCompleted", true) + } + startActivity(editIntent) + } catch (anfe: ActivityNotFoundException) { + Log.e("$TAG Failed to open native contact editor with URI [$uri]: $anfe") } - startActivity(editIntent) } } diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt index 0bb802a722..2700c107fe 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt @@ -19,6 +19,7 @@ */ package org.linphone.ui.main.settings.fragment +import android.content.ActivityNotFoundException import android.content.Intent import android.os.Bundle import android.provider.Settings @@ -159,14 +160,18 @@ class SettingsFragment : GenericMainFragment() { viewModel.goToIncomingCallNotificationChannelSettingsEvent.observe(viewLifecycleOwner) { it.consume { Log.w("$TAG Going to incoming call channel settings") - val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { - putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) - putExtra( - Settings.EXTRA_CHANNEL_ID, - getString(R.string.notification_channel_incoming_call_id) - ) + try { + val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { + putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) + putExtra( + Settings.EXTRA_CHANNEL_ID, + getString(R.string.notification_channel_incoming_call_id) + ) + } + startActivity(intent) + } catch (anfe: ActivityNotFoundException) { + Log.e("$TAG Failed to go to notification channel settings: $anfe") } - startActivity(intent) } } From 6e83b794b38759f9193a55d0ab7a4d92f8d7733b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Sun, 16 Mar 2025 20:53:21 +0100 Subject: [PATCH 016/593] Prevent crash if not on contact fragment before navigating to editor --- app/src/main/java/org/linphone/ui/GenericActivity.kt | 1 - .../ui/main/contacts/fragment/ContactFragment.kt | 11 +++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/GenericActivity.kt b/app/src/main/java/org/linphone/ui/GenericActivity.kt index 3471c2c282..6eb20d8d0f 100644 --- a/app/src/main/java/org/linphone/ui/GenericActivity.kt +++ b/app/src/main/java/org/linphone/ui/GenericActivity.kt @@ -41,7 +41,6 @@ import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.compatibility.Compatibility import org.linphone.core.tools.Log -import org.linphone.ui.main.settings.fragment.SettingsFragment import org.linphone.utils.ToastUtils import org.linphone.utils.slideInToastFromTop import org.linphone.utils.slideInToastFromTopForDuration diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt index 40950ee1e4..7ccb9d9d1d 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt @@ -185,10 +185,13 @@ class ContactFragment : SlidingPaneChildFragment() { viewModel.openLinphoneContactEditor.observe(viewLifecycleOwner) { it.consume { refKey -> - val action = ContactFragmentDirections.actionContactFragmentToEditContactFragment( - refKey - ) - findNavController().navigate(action) + if (findNavController().currentDestination?.id == R.id.contactFragment) { + val action = + ContactFragmentDirections.actionContactFragmentToEditContactFragment( + refKey + ) + findNavController().navigate(action) + } } } From ebb7201701ad57c232ec63324007f38a95fb8aa5 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Sun, 16 Mar 2025 20:57:19 +0100 Subject: [PATCH 017/593] Prevent crash if contacts listener triggers before chatRoom property is initialized --- .../chat/viewmodel/ConversationViewModel.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt index b97e7df55f..45c241ef76 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt @@ -402,6 +402,7 @@ class ConversationViewModel @UiThread fun updateUnreadMessageCount() { + if (!isChatRoomInitialized()) return coreContext.postOnCoreThread { unreadMessagesCount.postValue(chatRoom.unreadMessagesCount) } @@ -447,6 +448,7 @@ class ConversationViewModel @UiThread fun markAsRead() { + if (!isChatRoomInitialized()) return coreContext.postOnCoreThread { if (chatRoom.unreadMessagesCount == 0) return@postOnCoreThread Log.i("$TAG Marking chat room as read") @@ -456,6 +458,7 @@ class ConversationViewModel @UiThread fun mute() { + if (!isChatRoomInitialized()) return coreContext.postOnCoreThread { chatRoom.muted = true isMuted.postValue(chatRoom.muted) @@ -464,6 +467,7 @@ class ConversationViewModel @UiThread fun unMute() { + if (!isChatRoomInitialized()) return coreContext.postOnCoreThread { chatRoom.muted = false isMuted.postValue(chatRoom.muted) @@ -487,6 +491,7 @@ class ConversationViewModel @UiThread fun updateEphemeralLifetime(lifetime: Long) { + if (!isChatRoomInitialized()) return coreContext.postOnCoreThread { LinphoneUtils.chatRoomConfigureEphemeralMessagesLifetime(chatRoom, lifetime) ephemeralLifetime.postValue( @@ -500,6 +505,7 @@ class ConversationViewModel @UiThread fun loadMoreData(totalItemsCount: Int) { + if (!isChatRoomInitialized()) return coreContext.postOnCoreThread { val maxSize: Int = chatRoom.historyEventsSize Log.i("$TAG Loading more data, current total is $totalItemsCount, max size is $maxSize") @@ -535,6 +541,7 @@ class ConversationViewModel @WorkerThread fun checkIfConversationShouldBeDisabledForSecurityReasons() { + if (!isChatRoomInitialized()) return if (!chatRoom.hasCapability(ChatRoom.Capabilities.Encrypted.toInt())) { if (LinphoneUtils.getAccountForAddress(chatRoom.localAddress)?.params?.instantMessagingEncryptionMandatory == true) { Log.w( @@ -570,6 +577,8 @@ class ConversationViewModel @WorkerThread private fun configureChatRoom() { + if (!isChatRoomInitialized()) return + computeComposingLabel() isEndToEndEncrypted.postValue( @@ -585,6 +594,8 @@ class ConversationViewModel @WorkerThread private fun computeConversationInfo() { + if (!isChatRoomInitialized()) return + val group = LinphoneUtils.isChatRoomAGroup(chatRoom) isGroup.postValue(group) @@ -615,6 +626,8 @@ class ConversationViewModel @WorkerThread private fun computeParticipantsInfo() { + if (!isChatRoomInitialized()) return + val friends = arrayListOf() val address = if (chatRoom.hasCapability(ChatRoom.Capabilities.Basic.toInt())) { chatRoom.peerAddress @@ -644,6 +657,8 @@ class ConversationViewModel @WorkerThread private fun computeEvents() { + if (!isChatRoomInitialized()) return + eventsList.forEach(EventLogModel::destroy) val history = chatRoom.getHistoryEvents(MESSAGES_PER_PAGE) @@ -879,6 +894,7 @@ class ConversationViewModel @WorkerThread private fun computeComposingLabel() { + if (!isChatRoomInitialized()) return val composingFriends = arrayListOf() var label = "" for (address in chatRoom.composingAddresses) { @@ -928,6 +944,7 @@ class ConversationViewModel @WorkerThread private fun searchChatMessage(direction: SearchDirection) { + if (!isChatRoomInitialized()) return searchInProgress.postValue(true) val textToSearch = searchFilter.value.orEmpty().trim() From 8769a47ed07a644960427521a5308c98ab64c55c Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 17 Mar 2025 11:04:06 +0100 Subject: [PATCH 018/593] Adding back auto start setting --- .../java/org/linphone/core/CoreContext.kt | 26 +++++++++++++---- .../java/org/linphone/core/CorePreferences.kt | 7 +++++ .../settings/viewmodel/SettingsViewModel.kt | 12 ++++++++ .../org/linphone/utils/ActivityMonitor.kt | 6 ++++ .../res/layout/settings_advanced_fragment.xml | 29 ++++++++++++++++++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 76 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 70254af20b..4d39e2b3fc 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -123,6 +123,8 @@ class CoreContext MutableLiveData>>() } + private var keepAliveServiceStarted = false + @SuppressLint("HandlerLeak") private lateinit var coreThread: Handler @@ -548,15 +550,19 @@ class CoreContext Log.i("$TAG No configuration migration required") } - if (corePreferences.keepServiceAlive) { - Log.i("$TAG Starting keep alive service") - startKeepAliveService() - } - contactsManager.onCoreStarted(core) telecomManager.onCoreStarted(core) notificationsManager.onCoreStarted(core, oldVersion < 600000) // Re-create channels when migrating from a non 6.0 version Log.i("$TAG Started contacts, telecom & notifications managers") + + if (corePreferences.keepServiceAlive) { + if (activityMonitor.isInForeground() || corePreferences.autoStart) { + Log.i("$TAG Keep alive service is enabled and either app is in foreground or auto start is enabled, starting it") + startKeepAliveService() + } else { + Log.w("$TAG Keep alive service is enabled but auto start isn't and app is not in foreground, not starting it") + } + } } @WorkerThread @@ -644,6 +650,10 @@ class CoreContext Log.i("$TAG App is in foreground, PUBLISHING presence as Online") core.consolidatedPresence = ConsolidatedPresence.Online } + + if (corePreferences.keepServiceAlive && !keepAliveServiceStarted) { + startKeepAliveService() + } } } @@ -855,6 +865,10 @@ class CoreContext @WorkerThread fun startKeepAliveService() { + if (keepAliveServiceStarted) { + Log.w("$TAG Keep alive service already started, skipping") + } + val serviceIntent = Intent(Intent.ACTION_MAIN).setClass( context, CoreKeepAliveThirdPartyAccountsService::class.java @@ -862,6 +876,7 @@ class CoreContext Log.i("$TAG Starting Keep alive for third party accounts Service") try { context.startService(serviceIntent) + keepAliveServiceStarted = true } catch (e: Exception) { Log.e("$TAG Failed to start keep alive service: $e") } @@ -877,6 +892,7 @@ class CoreContext "$TAG Stopping Keep alive for third party accounts Service" ) context.stopService(serviceIntent) + keepAliveServiceStarted = false } @WorkerThread diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 269fc1949d..7c02742372 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -67,6 +67,13 @@ class CorePreferences config.setInt("app", "config_version", value) } + @get:WorkerThread @set:WorkerThread + var autoStart: Boolean + get() = config.getBool("app", "auto_start", true) + set(value) { + config.setBool("app", "auto_start", value) + } + @get:WorkerThread @set:WorkerThread var checkForUpdateServerUrl: String get() = config.getString("misc", "version_check_url_root", "").orEmpty() diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 3110077cc4..e065446aba 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -186,6 +186,7 @@ class SettingsViewModel ) // Advanced settings + val startAtBoot = MutableLiveData() val keepAliveThirdPartyAccountsService = MutableLiveData() val deviceName = MutableLiveData() @@ -311,6 +312,7 @@ class SettingsViewModel setupTunnel() } + startAtBoot.postValue(corePreferences.autoStart) keepAliveThirdPartyAccountsService.postValue(corePreferences.keepServiceAlive) deviceName.postValue(corePreferences.deviceName) @@ -653,6 +655,16 @@ class SettingsViewModel } } + @UiThread + fun toggleStartAtBoot() { + val newValue = startAtBoot.value == false + + coreContext.postOnCoreThread { + corePreferences.autoStart = newValue + startAtBoot.postValue(newValue) + } + } + @UiThread fun toggleKeepAliveThirdPartyAccountService() { val newValue = keepAliveThirdPartyAccountsService.value == false diff --git a/app/src/main/java/org/linphone/utils/ActivityMonitor.kt b/app/src/main/java/org/linphone/utils/ActivityMonitor.kt index d0ecc5ac6e..d7bb424f93 100644 --- a/app/src/main/java/org/linphone/utils/ActivityMonitor.kt +++ b/app/src/main/java/org/linphone/utils/ActivityMonitor.kt @@ -22,6 +22,7 @@ package org.linphone.utils import android.app.Activity import android.app.Application.ActivityLifecycleCallbacks import android.os.Bundle +import androidx.annotation.AnyThread import androidx.annotation.UiThread import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.core.tools.Log @@ -79,6 +80,11 @@ class ActivityMonitor : ActivityLifecycleCallbacks { activities.remove(activity) } + @AnyThread + fun isInForeground(): Boolean { + return mActive + } + private fun startInactivityChecker() { if (mLastChecker != null) mLastChecker!!.cancel() AndroidDispatcher.dispatchOnUIThreadAfter( diff --git a/app/src/main/res/layout/settings_advanced_fragment.xml b/app/src/main/res/layout/settings_advanced_fragment.xml index db969b5714..2e73f35550 100644 --- a/app/src/main/res/layout/settings_advanced_fragment.xml +++ b/app/src/main/res/layout/settings_advanced_fragment.xml @@ -60,6 +60,33 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> + + + + + app:layout_constraintTop_toBottomOf="@id/start_at_boot_switch"/> Auto Paramètres avancés + Démarrer au lancement du téléphone Garder l\'app en vie via un Service Nom de l\'appareil Caractères alpha-numériques uniquement diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2109e1fff5..6ab53dd0a0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -299,6 +299,7 @@ Auto Advanced settings + Start when device boots Keep app alive using Service Device ID Alpha-numerical characters only From b23f52adeca43bf3f57dfee5ad20f5a76c4b003f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Sun, 16 Mar 2025 18:57:35 +0100 Subject: [PATCH 019/593] Prevent system call notification to be stuck if call was ended in Linphone SDK before being added to TelecomManager's --- .../notifications/NotificationsManager.kt | 9 +- .../telecom/TelecomCallControlCallback.kt | 87 +++++++++++-------- 2 files changed, 58 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index e5012898ba..3c80ff6cb1 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -1127,19 +1127,20 @@ class NotificationsManager ) } - val channel = if (isIncoming) { + val channelId = if (isIncoming) { context.getString(R.string.notification_channel_incoming_call_id) } else { context.getString(R.string.notification_channel_call_id) } - + val channel = notificationManager.getNotificationChannel(channelId) + val importance = channel?.importance ?: NotificationManagerCompat.IMPORTANCE_NONE Log.i( - "Creating notification for ${if (isIncoming) "[incoming] " else ""}[${if (isConference) "conference" else "call"}] with video [${if (isVideo) "enabled" else "disabled"}] on channel [$channel]" + "Creating notification for ${if (isIncoming) "[incoming] " else ""}[${if (isConference) "conference" else "call"}] with video [${if (isVideo) "enabled" else "disabled"}] on channel [$channel] with importance [$importance]" ) val builder = NotificationCompat.Builder( context, - channel + channelId ).apply { try { style.setIsVideo(isVideo) diff --git a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt index 6d551839d0..b99043b2eb 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt @@ -82,41 +82,9 @@ class TelecomCallControlCallback( } } } else if (state == Call.State.End) { - val reason = call.reason - val direction = call.dir - scope.launch { - val disconnectCause = when (reason) { - Reason.NotAnswered -> DisconnectCause.REMOTE - Reason.Declined -> DisconnectCause.REJECTED - Reason.Busy -> { - if (direction == Call.Dir.Incoming) { - DisconnectCause.MISSED - } else { - DisconnectCause.BUSY - } - } - else -> DisconnectCause.LOCAL - } - Log.i("$TAG Disconnecting [${if (direction == Call.Dir.Incoming)"incoming" else "outgoing"}] call with cause [${disconnectCauseToString(disconnectCause)}] because it has ended with reason [$reason]") - try { - callControl.disconnect(DisconnectCause(disconnectCause)) - } catch (ise: IllegalArgumentException) { - Log.e("$TAG Couldn't disconnect call control with cause [${disconnectCauseToString(disconnectCause)}]: $ise") - } - } + callEnded() } else if (state == Call.State.Error) { - val reason = call.reason - scope.launch { - // For some reason DisconnectCause.ERROR or DisconnectCause.BUSY triggers an IllegalArgumentException with following message - // Valid DisconnectCause codes are limited to [DisconnectCause.LOCAL, DisconnectCause.REMOTE, DisconnectCause.MISSED, or DisconnectCause.REJECTED] - val disconnectCause = DisconnectCause.REJECTED - Log.w("$TAG Disconnecting call with cause [${disconnectCauseToString(disconnectCause)}] due to error [$message] and reason [$reason]") - try { - callControl.disconnect(DisconnectCause(disconnectCause)) - } catch (ise: IllegalArgumentException) { - Log.e("$TAG Couldn't disconnect call control with cause [${disconnectCauseToString(disconnectCause)}]: $ise") - } - } + callError(message) } else if (state == Call.State.Pausing) { scope.launch { Log.i("$TAG Pausing call") @@ -144,6 +112,17 @@ class TelecomCallControlCallback( "$TAG Callback have been set for call, Telecom call ID is [${callControl.getCallId()}]" ) + coreContext.postOnCoreThread { + val state = call.state + Log.i("$TAG Call state currently is [$state]") + when (state) { + Call.State.End -> callEnded() + Call.State.Error -> callError("") + Call.State.Released -> callEnded() + else -> {} // doing nothing + } + } + callControl.availableEndpoints.onEach { list -> Log.i("$TAG New available audio endpoints list") if (availableEndpoints != list) { @@ -303,6 +282,46 @@ class TelecomCallControlCallback( return false } + private fun callEnded() { + val reason = call.reason + val direction = call.dir + scope.launch { + val disconnectCause = when (reason) { + Reason.NotAnswered -> DisconnectCause.REMOTE + Reason.Declined -> DisconnectCause.REJECTED + Reason.Busy -> { + if (direction == Call.Dir.Incoming) { + DisconnectCause.MISSED + } else { + DisconnectCause.BUSY + } + } + else -> DisconnectCause.LOCAL + } + Log.i("$TAG Disconnecting [${if (direction == Call.Dir.Incoming)"incoming" else "outgoing"}] call with cause [${disconnectCauseToString(disconnectCause)}] because it has ended with reason [$reason]") + try { + callControl.disconnect(DisconnectCause(disconnectCause)) + } catch (ise: IllegalArgumentException) { + Log.e("$TAG Couldn't disconnect call control with cause [${disconnectCauseToString(disconnectCause)}]: $ise") + } + } + } + + private fun callError(message: String) { + val reason = call.reason + scope.launch { + // For some reason DisconnectCause.ERROR or DisconnectCause.BUSY triggers an IllegalArgumentException with following message + // Valid DisconnectCause codes are limited to [DisconnectCause.LOCAL, DisconnectCause.REMOTE, DisconnectCause.MISSED, or DisconnectCause.REJECTED] + val disconnectCause = DisconnectCause.REJECTED + Log.w("$TAG Disconnecting call with cause [${disconnectCauseToString(disconnectCause)}] due to error [$message] and reason [$reason]") + try { + callControl.disconnect(DisconnectCause(disconnectCause)) + } catch (ise: IllegalArgumentException) { + Log.e("$TAG Couldn't disconnect call control with cause [${disconnectCauseToString(disconnectCause)}]: $ise") + } + } + } + private fun disconnectCauseToString(cause: Int): String { return when (cause) { DisconnectCause.UNKNOWN -> "UNKNOWN" From 6c6fb9eff381f1f6e767141bebf37fe60acd0ad4 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 17 Mar 2025 13:10:38 +0100 Subject: [PATCH 020/593] Prevent call transfer if state is Ended, Error or Released --- .../ui/call/viewmodel/CurrentCallViewModel.kt | 17 +++++++++++++++++ .../java/org/linphone/utils/LinphoneUtils.kt | 3 ++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 4677606c22..cb799c5475 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -945,6 +945,17 @@ class CurrentCallViewModel @WorkerThread fun attendedTransferCallTo(to: Call) { if (::currentCall.isInitialized) { + val toCallState = to.state + if (LinphoneUtils.isCallEnding(toCallState, considerReleasedAsEnding = true)) { + Log.e("$TAG Do not attempt attended transfer to call in state [$toCallState]") + return + } + val currentCallState = currentCall.state + if (LinphoneUtils.isCallEnding(currentCallState, considerReleasedAsEnding = true)) { + Log.e("$TAG Do not attempt attended transfer of call in state [$currentCallState]") + return + } + Log.i( "$TAG Doing an attended transfer between currently displayed call [${currentCall.remoteAddress.asStringUriOnly()}] and paused call [${to.remoteAddress.asStringUriOnly()}]" ) @@ -959,6 +970,12 @@ class CurrentCallViewModel @WorkerThread fun blindTransferCallTo(to: Address) { if (::currentCall.isInitialized) { + val callState = currentCall.state + if (LinphoneUtils.isCallEnding(callState, considerReleasedAsEnding = true)) { + Log.e("$TAG Do not attempt blind transfer of call in state [$callState]") + return + } + Log.i( "$TAG Call [${currentCall.remoteAddress.asStringUriOnly()}] is being blindly transferred to [${to.asStringUriOnly()}]" ) diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index fe594e7fd1..5ee6aae335 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -168,9 +168,10 @@ class LinphoneUtils { } @AnyThread - fun isCallEnding(callState: Call.State): Boolean { + fun isCallEnding(callState: Call.State, considerReleasedAsEnding: Boolean = false): Boolean { return when (callState) { Call.State.End, Call.State.Error -> true + Call.State.Released -> considerReleasedAsEnding else -> false } } From 052d7cc52266ee25f65e9566203c111faed8b923 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 17 Mar 2025 14:19:14 +0100 Subject: [PATCH 021/593] Added UI setting to have dialpad automatically opened when starting new call --- .../java/org/linphone/core/CorePreferences.kt | 11 ++++--- .../settings/viewmodel/SettingsViewModel.kt | 12 ++++++++ .../res/layout/settings_user_interface.xml | 30 ++++++++++++++++++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 7c02742372..4b2bdb07b8 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -234,6 +234,13 @@ class CorePreferences config.setBool("ui", "enable_secure_mode", value) } + @get:WorkerThread @set:WorkerThread + var automaticallyShowDialpad: Boolean + get() = config.getBool("ui", "automatically_show_dialpad", false) + set(value) { + config.setBool("ui", "automatically_show_dialpad", value) + } + @get:WorkerThread @set:WorkerThread var themeMainColor: String get() = config.getString("ui", "theme_main_color", "orange")!! @@ -325,10 +332,6 @@ class CorePreferences val fetchContactsFromDefaultDirectory: Boolean get() = config.getBool("app", "fetch_contacts_from_default_directory", true) - @get:WorkerThread - val automaticallyShowDialpad: Boolean - get() = config.getBool("ui", "automatically_show_dialpad", false) - @get:WorkerThread val showLettersOnDialpad: Boolean get() = config.getBool("ui", "show_letters_on_dialpad", true) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index e065446aba..653d513863 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -142,6 +142,8 @@ class SettingsViewModel val allowIpv6 = MutableLiveData() // User Interface settings + val autoShowDialpad = MutableLiveData() + val showThemeSelector = MutableLiveData() val theme = MutableLiveData() val availableThemesNames = arrayListOf( @@ -305,6 +307,7 @@ class SettingsViewModel defaultLayout.postValue(core.defaultConferenceLayout.toInt()) + autoShowDialpad.postValue(corePreferences.automaticallyShowDialpad) theme.postValue(corePreferences.darkMode) color.postValue(corePreferences.themeMainColor) @@ -580,6 +583,15 @@ class SettingsViewModel expandUserInterface.value = expandUserInterface.value == false } + @UiThread + fun toggleAutoShowDialpad() { + val newValue = autoShowDialpad.value == false + coreContext.postOnCoreThread { core -> + corePreferences.automaticallyShowDialpad = newValue + autoShowDialpad.postValue(newValue) + } + } + @UiThread fun setTheme(themeValue: Int) { coreContext.postOnCoreThread { diff --git a/app/src/main/res/layout/settings_user_interface.xml b/app/src/main/res/layout/settings_user_interface.xml index 467b786933..f392d25936 100644 --- a/app/src/main/res/layout/settings_user_interface.xml +++ b/app/src/main/res/layout/settings_user_interface.xml @@ -28,6 +28,34 @@ android:visibility="@{viewModel.showColorSelector ? View.VISIBLE : View.GONE}" app:constraint_referenced_ids="color_spinner, color_spinner_caret, color_title" /> + + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 39768eb43c..e2d41d9e1f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -243,6 +243,7 @@ Se connecter uniquement via le Wi-Fi Autoriser l\'IPv6 Affichage + Ouvrir le pavé numérique automatiquement Thème Sombre Clair diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6ab53dd0a0..d72a82dc09 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -282,6 +282,7 @@ Use only Wi-Fi networks Allow IPv6 User interface + Automatically open dialpad Theme Dark theme Light theme From 8a4956e7c1b11915efb77d6c6ab817da9a15f7f7 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 17 Mar 2025 14:21:54 +0100 Subject: [PATCH 022/593] Hidden save/export buttons for call recordings until export feature will be added to SDK --- app/src/main/res/layout/recording_player_fragment.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/layout/recording_player_fragment.xml b/app/src/main/res/layout/recording_player_fragment.xml index aaff303820..49717b335b 100644 --- a/app/src/main/res/layout/recording_player_fragment.xml +++ b/app/src/main/res/layout/recording_player_fragment.xml @@ -162,6 +162,7 @@ android:padding="15dp" android:src="@drawable/share_network" android:contentDescription="@string/content_description_share_file" + android:visibility="gone" app:tint="@color/gray_main2_500" app:layout_constraintEnd_toStartOf="@id/save" app:layout_constraintTop_toTopOf="parent" /> @@ -175,6 +176,7 @@ android:padding="15dp" android:src="@drawable/download_simple" android:contentDescription="@string/content_description_save_file" + android:visibility="gone" app:tint="@color/gray_main2_500" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> From 9837a834d452577e3a4cfc29e389d643d62a770b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 17 Mar 2025 14:37:54 +0100 Subject: [PATCH 023/593] Added back "replace + by 00 when formatting phone numbers" account setting --- .../viewmodel/AccountSettingsViewModel.kt | 4 +++ .../java/org/linphone/utils/AudioUtils.kt | 2 +- .../res/layout/account_settings_fragment.xml | 29 ++++++++++++++++++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt index ea3c01c3b7..bf9ef2f4f4 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt @@ -90,6 +90,8 @@ class AccountSettingsViewModel val mwiUri = MutableLiveData() val voicemailUri = MutableLiveData() + val replacePlusBy00 = MutableLiveData() + val cpimInBasicChatRooms = MutableLiveData() val accountFoundEvent = MutableLiveData>() @@ -165,6 +167,7 @@ class AccountSettingsViewModel mwiUri.postValue(params.mwiServerAddress?.asStringUriOnly().orEmpty()) voicemailUri.postValue(params.voicemailAddress?.asStringUriOnly().orEmpty()) + replacePlusBy00.postValue(params.isDialEscapePlusEnabled) expire.postValue(params.expires.toString()) @@ -298,6 +301,7 @@ class AccountSettingsViewModel newParams.ccmpServerUrl = ccmpServerUrl.value newParams.limeServerUrl = limeServerUrl.value + newParams.isDialEscapePlusEnabled = replacePlusBy00.value == true account.params = newParams Log.i("$TAG Changes have been saved") diff --git a/app/src/main/java/org/linphone/utils/AudioUtils.kt b/app/src/main/java/org/linphone/utils/AudioUtils.kt index d3057923d2..cdf8025733 100644 --- a/app/src/main/java/org/linphone/utils/AudioUtils.kt +++ b/app/src/main/java/org/linphone/utils/AudioUtils.kt @@ -230,7 +230,7 @@ class AudioUtils { } } Log.i( - "$TAG Found headset/headphones/hearingAid sound card [$headsetCard], bluetooth sound card [$bluetoothCard] and microphone card [$microphoneCard]" + "$TAG Found headset/headphones sound card [$headsetCard], bluetooth/hearingAid sound card [$bluetoothCard] and microphone card [$microphoneCard]" ) return headsetCard ?: bluetoothCard ?: microphoneCard } diff --git a/app/src/main/res/layout/account_settings_fragment.xml b/app/src/main/res/layout/account_settings_fragment.xml index 655aad1dcb..904c344828 100644 --- a/app/src/main/res/layout/account_settings_fragment.xml +++ b/app/src/main/res/layout/account_settings_fragment.xml @@ -184,7 +184,6 @@ android:layout_height="50dp" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" - android:layout_marginBottom="20dp" android:background="@drawable/edit_text_background" android:paddingStart="20dp" android:paddingEnd="20dp" @@ -194,6 +193,34 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/mwi_uri_title" + app:layout_constraintBottom_toTopOf="@id/replace_plus_by_00_switch"/> + + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index e2d41d9e1f..e42b948607 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -331,6 +331,7 @@ Utiliser CPIM dans les conversations \"basiques\" URI de la messagerie vocale URI du serveur MWI (Message Waiting Indicator) + Remplacer + par 00 lors du formattage des numéros de téléphone Mettre à jour le mot de passe Autentification requise diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d72a82dc09..de6bc47d70 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -370,6 +370,7 @@ Use CPIM in \"basic\" conversations Voicemail URI MWI server URI (Message Waiting Indicator) + Replace + by 00 when formatting phone numbers Update password Authentication needed From b5a1e21f408989b089eefa2943956389b9c1437f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 17 Mar 2025 16:09:23 +0100 Subject: [PATCH 024/593] Should fix quit button visibility in drawer menu --- .../linphone/ui/main/fragment/DrawerMenuFragment.kt | 8 ++++++++ .../settings/fragment/SettingsAdvancedFragment.kt | 7 +++++++ .../ui/main/settings/viewmodel/SettingsViewModel.kt | 5 +++++ .../ui/main/viewmodel/DrawerMenuViewModel.kt | 12 +++++++++++- .../ui/main/viewmodel/SharedMainViewModel.kt | 4 ++++ 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/main/fragment/DrawerMenuFragment.kt b/app/src/main/java/org/linphone/ui/main/fragment/DrawerMenuFragment.kt index 95fccd147a..a85fa47853 100644 --- a/app/src/main/java/org/linphone/ui/main/fragment/DrawerMenuFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/fragment/DrawerMenuFragment.kt @@ -170,6 +170,14 @@ class DrawerMenuFragment : GenericMainFragment() { } } } + + sharedViewModel.refreshDrawerMenuQuitButtonEvent.observe(viewLifecycleOwner) { + it.consume { + coreContext.postOnCoreThread { + viewModel.checkIfKeepAliveServiceIsEnabled() + } + } + } } private fun showAccountPopupMenu(view: View, account: Account) { diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedFragment.kt index 9c92dbd74c..733b7360c2 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedFragment.kt @@ -32,6 +32,7 @@ import org.linphone.databinding.SettingsAdvancedFragmentBinding import org.linphone.ui.GenericActivity import org.linphone.ui.main.fragment.GenericMainFragment import org.linphone.ui.main.settings.viewmodel.SettingsViewModel +import org.linphone.utils.Event @UiThread class SettingsAdvancedFragment : GenericMainFragment() { @@ -105,6 +106,12 @@ class SettingsAdvancedFragment : GenericMainFragment() { setupOutputAudioDevicePicker() } + viewModel.keepAliveServiceSettingChangedEvent.observe(viewLifecycleOwner) { + it.consume { + sharedViewModel.refreshDrawerMenuQuitButtonEvent.postValue(Event(true)) + } + } + startPostponedEnterTransition() } diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 653d513863..0f39a1c2bd 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -64,6 +64,10 @@ class SettingsViewModel MutableLiveData>() } + val keepAliveServiceSettingChangedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + // Security settings val isVfsEnabled = MutableLiveData() @@ -689,6 +693,7 @@ class SettingsViewModel } else { coreContext.stopKeepAliveService() } + keepAliveServiceSettingChangedEvent.postValue(Event(true)) } } diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/DrawerMenuViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/DrawerMenuViewModel.kt index 54cd3452fe..eb01e13f6f 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/DrawerMenuViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/DrawerMenuViewModel.kt @@ -126,7 +126,8 @@ class DrawerMenuViewModel hideRecordings.postValue(corePreferences.disableCallRecordings) hideSettings.postValue(corePreferences.hideSettings) - hideQuitButton.postValue(!corePreferences.keepServiceAlive) + + checkIfKeepAliveServiceIsEnabled() computeAccountsList() computeShortcuts() @@ -169,6 +170,15 @@ class DrawerMenuViewModel } } + @WorkerThread + fun checkIfKeepAliveServiceIsEnabled() { + val useKeepAliveService = corePreferences.keepServiceAlive + hideQuitButton.postValue(!useKeepAliveService) + if (useKeepAliveService) { + Log.i("$TAG Keep alive service is enabled, do not hide quit button") + } + } + @WorkerThread private fun computeAccountsList() { accounts.value.orEmpty().forEach(AccountModel::destroy) diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt index 2ce556ef59..61aa32472f 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt @@ -74,6 +74,10 @@ class SharedMainViewModel MutableLiveData>() } + val refreshDrawerMenuQuitButtonEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + val forceUpdateAvailableNavigationItems: MutableLiveData> by lazy { MutableLiveData>() } From 0d8397b9148fc14a0c3ef80ceb6a556db1861a29 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 17 Mar 2025 15:29:25 +0100 Subject: [PATCH 025/593] Should fix hearing aids issue --- .../java/org/linphone/telecom/TelecomCallControlCallback.kt | 2 +- .../org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt | 5 ++++- app/src/main/java/org/linphone/utils/AudioUtils.kt | 5 +++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt index b99043b2eb..c4051cc7d4 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt @@ -229,7 +229,7 @@ class TelecomCallControlCallback( routes.find { it == AudioDevice.Type.Speaker } } CallEndpointCompat.Companion.TYPE_BLUETOOTH -> { - routes.find { it == AudioDevice.Type.Bluetooth } + routes.find { it == AudioDevice.Type.Bluetooth || it == AudioDevice.Type.HearingAid } } CallEndpointCompat.Companion.TYPE_WIRED_HEADSET -> { wiredHeadsetFound = true diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index cb799c5475..2c94289fc0 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -757,7 +757,10 @@ class CurrentCallViewModel AudioDevice.Type.Headset, AudioDevice.Type.Headphones -> AudioUtils.routeAudioToHeadset( currentCall ) - AudioDevice.Type.Bluetooth, AudioDevice.Type.HearingAid -> AudioUtils.routeAudioToBluetooth( + AudioDevice.Type.Bluetooth -> AudioUtils.routeAudioToBluetooth( + currentCall + ) + AudioDevice.Type.HearingAid -> AudioUtils.routeAudioToHearingAid( currentCall ) AudioDevice.Type.Speaker -> AudioUtils.routeAudioToSpeaker( diff --git a/app/src/main/java/org/linphone/utils/AudioUtils.kt b/app/src/main/java/org/linphone/utils/AudioUtils.kt index cdf8025733..70175686f8 100644 --- a/app/src/main/java/org/linphone/utils/AudioUtils.kt +++ b/app/src/main/java/org/linphone/utils/AudioUtils.kt @@ -50,6 +50,11 @@ class AudioUtils { routeAudioTo(call, arrayListOf(AudioDevice.Type.Bluetooth)) } + @WorkerThread + fun routeAudioToHearingAid(call: Call? = null) { + routeAudioTo(call, arrayListOf(AudioDevice.Type.HearingAid)) + } + @WorkerThread fun routeAudioToHeadset(call: Call? = null) { routeAudioTo( From 915a8470839e06add19f15917c4a381d52dc8f23 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 17 Mar 2025 17:05:16 +0100 Subject: [PATCH 026/593] Prevent crash due to service being started as foreground if post_notifications permission isn't granted --- .../notifications/NotificationsManager.kt | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 3c80ff6cb1..b79b8b68c0 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -690,13 +690,21 @@ class NotificationsManager Log.i( "$TAG Service found, starting it as foreground using notification ID [$INCOMING_CALL_ID] with type PHONE_CALL" ) - Compatibility.startServiceForeground( - service, - INCOMING_CALL_ID, - notification, - Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL - ) - currentInCallServiceNotificationId = INCOMING_CALL_ID + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + Compatibility.startServiceForeground( + service, + INCOMING_CALL_ID, + notification, + Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL + ) + currentInCallServiceNotificationId = INCOMING_CALL_ID + } else { + Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground service!") + } } else { Log.w("$TAG Core Foreground Service hasn't started yet...") } @@ -785,16 +793,24 @@ class NotificationsManager } } - Log.i( - "$TAG Service found, starting it as foreground using notification ID [${notifiable.notificationId}] with type(s) [$mask]" - ) - Compatibility.startServiceForeground( - service, - notifiable.notificationId, - notification, - mask - ) - currentInCallServiceNotificationId = notifiable.notificationId + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) { + Log.i( + "$TAG Service found, starting it as foreground using notification ID [${notifiable.notificationId}] with type(s) [$mask]" + ) + Compatibility.startServiceForeground( + service, + notifiable.notificationId, + notification, + mask + ) + currentInCallServiceNotificationId = notifiable.notificationId + } else { + Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground service!") + } } @WorkerThread From d6c6de2b5e0a2dd959b0ca37e493ea532a4761f2 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 17 Mar 2025 17:36:18 +0100 Subject: [PATCH 027/593] Wait for foreground service to be started before being stopped to try preventing ForegroundServiceDidNotStartInTimeException/RemoteServiceException due to Context.startForegroundService() did not then call Service.startForeground() --- .../notifications/NotificationsManager.kt | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index b79b8b68c0..553000ad53 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -114,6 +114,9 @@ class NotificationsManager } private var inCallService: CoreInCallService? = null + private var inCallServiceForegroundNotificationPublished = false + private var waitForInCallServiceForegroundToStopIt = false + private var keepAliveService: CoreKeepAliveThirdPartyAccountsService? = null private val callNotificationsMap: HashMap = HashMap() @@ -255,7 +258,13 @@ class NotificationsManager @WorkerThread override fun onLastCallEnded(core: Core) { Log.i("$TAG Last call ended, stopping foreground service") - stopInCallCallForegroundService() + if (inCallServiceForegroundNotificationPublished) { + stopInCallCallForegroundService() + } else { + // Wait for foreground service to have been started before stopping it + Log.w("$TAG We would like to stop the foreground service but it wasn't started yet, wait for it") + waitForInCallServiceForegroundToStopIt = true + } } @WorkerThread @@ -485,7 +494,13 @@ class NotificationsManager coreContext.postOnCoreThread { core -> if (core.callsNb == 0) { Log.w("$TAG No call anymore, stopping service") - stopInCallCallForegroundService() + if (inCallServiceForegroundNotificationPublished) { + stopInCallCallForegroundService() + } else { + // Wait for foreground service to have been started before stopping it + Log.w("$TAG We would like to stop the foreground service but it wasn't started yet, wait for it") + waitForInCallServiceForegroundToStopIt = true + } } else if (currentInCallServiceNotificationId == -1) { Log.i( "$TAG At least a call is still running and no foreground Service notification was found" @@ -702,6 +717,11 @@ class NotificationsManager Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL ) currentInCallServiceNotificationId = INCOMING_CALL_ID + inCallServiceForegroundNotificationPublished = true + if (waitForInCallServiceForegroundToStopIt) { + Log.i("$TAG We were waiting for foreground service to be started to stop it, doing it") + stopInCallCallForegroundService() + } } else { Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground service!") } @@ -808,6 +828,11 @@ class NotificationsManager mask ) currentInCallServiceNotificationId = notifiable.notificationId + inCallServiceForegroundNotificationPublished = true + if (waitForInCallServiceForegroundToStopIt) { + Log.i("$TAG We were waiting for foreground service to be started to stop it, doing it") + stopInCallCallForegroundService() + } } else { Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground service!") } @@ -822,6 +847,7 @@ class NotificationsManager ) service.stopForeground(STOP_FOREGROUND_REMOVE) service.stopSelf() + inCallServiceForegroundNotificationPublished = false } else { Log.w("$TAG Can't stop foreground Service & notif, no Service was found") } From be5428aa08a15a3526b65be249ce00c0aae2669d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 18 Mar 2025 10:12:49 +0100 Subject: [PATCH 028/593] Add generic exception handle for starting action_view activity for URL --- .../ui/assistant/fragment/LandingFragment.kt | 12 ++++++++++++ .../ui/assistant/fragment/RegisterFragment.kt | 4 ++++ .../ThirdPartySipAccountWarningFragment.kt | 4 ++++ .../ui/main/help/fragment/HelpFragment.kt | 16 ++++++++++++++++ 4 files changed, 36 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt index 017875fea7..e252398724 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt @@ -115,6 +115,10 @@ class LandingFragment : GenericFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" ) + } catch (e: Exception) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" + ) } } @@ -222,6 +226,10 @@ class LandingFragment : GenericFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" ) + } catch (e: Exception) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" + ) } } } @@ -240,6 +248,10 @@ class LandingFragment : GenericFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" ) + } catch (e: Exception) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" + ) } } } diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt index 693bf485d3..4a0d49cd13 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt @@ -113,6 +113,10 @@ class RegisterFragment : GenericFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" ) + } catch (e: Exception) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" + ) } } diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountWarningFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountWarningFragment.kt index 050caff265..2fa8e23e12 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountWarningFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountWarningFragment.kt @@ -72,6 +72,10 @@ class ThirdPartySipAccountWarningFragment : GenericFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" ) + } catch (e: Exception) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" + ) } } diff --git a/app/src/main/java/org/linphone/ui/main/help/fragment/HelpFragment.kt b/app/src/main/java/org/linphone/ui/main/help/fragment/HelpFragment.kt index 3d7e7c43ba..4413b92b0e 100644 --- a/app/src/main/java/org/linphone/ui/main/help/fragment/HelpFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/help/fragment/HelpFragment.kt @@ -89,6 +89,10 @@ class HelpFragment : GenericMainFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" ) + } catch (e: Exception) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" + ) } } @@ -105,6 +109,10 @@ class HelpFragment : GenericMainFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" ) + } catch (e: Exception) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" + ) } } @@ -121,6 +129,10 @@ class HelpFragment : GenericMainFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" ) + } catch (e: Exception) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" + ) } } @@ -180,6 +192,10 @@ class HelpFragment : GenericMainFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" ) + } catch (e: Exception) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" + ) } dialog.dismiss() } From 2a5b5d368c1a711631ff2cd4b0cfd36f406369ba Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 17 Mar 2025 15:49:51 +0100 Subject: [PATCH 029/593] Revert using notification channel to play incoming calls ringtone --- app/src/main/assets/linphonerc_factory | 2 +- .../notifications/NotificationsManager.kt | 27 ++++++++++- .../settings/fragment/SettingsFragment.kt | 45 ++++++++++++++++--- .../settings/viewmodel/SettingsViewModel.kt | 21 ++++++++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 2 + 6 files changed, 86 insertions(+), 12 deletions(-) diff --git a/app/src/main/assets/linphonerc_factory b/app/src/main/assets/linphonerc_factory index 31d406b173..9002c427b2 100644 --- a/app/src/main/assets/linphonerc_factory +++ b/app/src/main/assets/linphonerc_factory @@ -26,7 +26,7 @@ update_presence_model_timestamp_before_publish_expires_refresh=1 [sound] #remove this property for any application that is not Linphone public version itself ec_calibrator_cool_tones=1 -disable_ringing=1 +disable_ringing=0 [audio] android_disable_audio_focus_requests=1 diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 553000ad53..107c731842 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -544,10 +544,21 @@ class NotificationsManager Log.e("$TAG Failed to delete notification channel ID [${channel.id}]: $e") } } + } else { + try { + val oldId = context.getString(R.string.notification_channel_without_ringtone_incoming_call_id) + val oldChannel = notificationManager.getNotificationChannel(oldId) + if (oldChannel != null) { + Log.i("$TAG Deleting notification channel ID [$oldId]") + notificationManager.deleteNotificationChannel(oldId) + } + } catch (e: Exception) { + Log.e("$TAG Failed to check if deprecated incoming call notification channel exists: $e") + } } createThirdPartyAccountKeepAliveServiceChannel() - createIncomingCallNotificationChannel() + createIncomingCallNotificationChannelWithoutRingtone() createMissedCallNotificationChannel() createActiveCallNotificationChannel() createMessageChannel() @@ -1170,7 +1181,7 @@ class NotificationsManager } val channelId = if (isIncoming) { - context.getString(R.string.notification_channel_incoming_call_id) + context.getString(R.string.notification_channel_without_ringtone_incoming_call_id) } else { context.getString(R.string.notification_channel_call_id) } @@ -1607,6 +1618,18 @@ class NotificationsManager notificationManager.createNotificationChannel(channel) } + @MainThread + private fun createIncomingCallNotificationChannelWithoutRingtone() { + val id = context.getString(R.string.notification_channel_without_ringtone_incoming_call_id) + val name = context.getString(R.string.notification_channel_incoming_call_name) + + val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH).apply { + description = name + lockscreenVisibility = Notification.VISIBILITY_PUBLIC + } + notificationManager.createNotificationChannel(channel) + } + @MainThread private fun createMissedCallNotificationChannel() { val id = context.getString(R.string.notification_channel_missed_call_id) diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt index 2700c107fe..a521407faf 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt @@ -19,10 +19,11 @@ */ package org.linphone.ui.main.settings.fragment -import android.content.ActivityNotFoundException +import android.app.Activity import android.content.Intent +import android.media.RingtoneManager +import android.net.Uri import android.os.Bundle -import android.provider.Settings import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -38,13 +39,17 @@ import org.linphone.databinding.SettingsFragmentBinding import org.linphone.ui.main.fragment.GenericMainFragment import org.linphone.utils.ConfirmationDialogModel import org.linphone.ui.main.settings.viewmodel.SettingsViewModel +import org.linphone.utils.AppUtils import org.linphone.utils.DialogUtils import org.linphone.utils.Event +import java.lang.Exception @UiThread class SettingsFragment : GenericMainFragment() { companion object { private const val TAG = "[Settings Fragment]" + + private const val RINGTONE_PICKER_INTENT_ID = 89 } private lateinit var binding: SettingsFragmentBinding @@ -158,19 +163,31 @@ class SettingsFragment : GenericMainFragment() { } viewModel.goToIncomingCallNotificationChannelSettingsEvent.observe(viewLifecycleOwner) { - it.consume { - Log.w("$TAG Going to incoming call channel settings") + it.consume { currentRingtone -> try { + /* + Log.w("$TAG Going to incoming call channel settings") val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) putExtra( Settings.EXTRA_CHANNEL_ID, - getString(R.string.notification_channel_incoming_call_id) + getString(R.string.notification_channel_without_ringtone_incoming_call_id) ) } startActivity(intent) - } catch (anfe: ActivityNotFoundException) { - Log.e("$TAG Failed to go to notification channel settings: $anfe") + */ + val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { + putExtra( + RingtoneManager.EXTRA_RINGTONE_TYPE, + RingtoneManager.TYPE_RINGTONE + ) + putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, currentRingtone) + putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, AppUtils.getString(R.string.settings_calls_change_ringtone_pick_title)) + } + startActivityForResult(intent, RINGTONE_PICKER_INTENT_ID) + } catch (e: Exception) { + Log.e("$TAG Failed start ringtone picker: $e") + // TODO: show error to user } } } @@ -308,6 +325,20 @@ class SettingsFragment : GenericMainFragment() { startPostponedEnterTransition() } + @Deprecated("Deprecated in Java") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (resultCode == Activity.RESULT_OK && requestCode == RINGTONE_PICKER_INTENT_ID) { + val uri: Uri? = data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) + if (uri != null) { + Log.i("$TAG Ringtone picker result is OK, URI found in intent is [$uri]") + viewModel.setRingtoneUri(uri) + } else { + Log.e("$TAG Ringtone picker result is OK but URI is null!") + // TODO: show error to user + } + } + } + override fun onResume() { super.onResume() diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 0f39a1c2bd..1da989a840 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -19,9 +19,12 @@ */ package org.linphone.ui.main.settings.viewmodel +import android.media.RingtoneManager +import android.net.Uri import android.os.Vibrator import androidx.annotation.UiThread import androidx.annotation.WorkerThread +import androidx.core.net.toUri import androidx.lifecycle.MutableLiveData import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences @@ -87,7 +90,7 @@ class SettingsViewModel val autoRecordCalls = MutableLiveData() - val goToIncomingCallNotificationChannelSettingsEvent = MutableLiveData>() + val goToIncomingCallNotificationChannelSettingsEvent = MutableLiveData>() // Conversations settings val showConversationsSettings = MutableLiveData() @@ -445,7 +448,21 @@ class SettingsViewModel @UiThread fun changeRingtone() { - goToIncomingCallNotificationChannelSettingsEvent.value = Event(true) + coreContext.postOnCoreThread { core -> + val defaultDeviceRingtone = RingtoneManager.getActualDefaultRingtoneUri(coreContext.context, RingtoneManager.TYPE_RINGTONE) + val coreRingtone = core.ring?.toUri() + Log.i("$TAG Currently set ringtone in Core is [$coreRingtone], device default ringtone is [$defaultDeviceRingtone]") + val currentRingtone = coreRingtone ?: defaultDeviceRingtone + goToIncomingCallNotificationChannelSettingsEvent.postValue(Event(currentRingtone)) + } + } + + @UiThread + fun setRingtoneUri(ringtone: Uri) { + coreContext.postOnCoreThread { core -> + core.ring = ringtone.toString() + Log.i("$TAG Newly set ringtone is [${core.ring}]") + } } @UiThread diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index e42b948607..1dc296698b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -200,6 +200,7 @@ Vibration lors de l\'appel Enregistrement automatique des appels Changer de sonnerie + Choisissez la sonnerie Conversations Télécharger automatiquement les fichiers Rendre visible dans la galerie les médias téléchargés diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index de6bc47d70..6abd1cfae3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,6 +18,7 @@ linphone_6.0_notification_missed_call_id linphone_6.0_notification_service_id linphone_6.0_notification_chat_id + linphone_6.0.1_notification_incoming_call_id ❤️ 👍 @@ -239,6 +240,7 @@ Vibrate while incoming call is ringing Automatically start recording calls Change ringtone + Pick ringtone Conversations Auto-download files Make downloaded media public From 886be9e0380fb96e0283461482eb822ec9d940bd Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 18 Mar 2025 11:45:52 +0100 Subject: [PATCH 030/593] Fixed hearing aid icon not showing in bottom actions when selected + updated earpiece icon in device list to match --- app/src/main/java/org/linphone/core/CoreContext.kt | 12 +++++++++--- .../linphone/telecom/TelecomCallControlCallback.kt | 7 ++++--- .../ui/call/viewmodel/CurrentCallViewModel.kt | 12 ++++++++++++ app/src/main/res/layout/call_actions_generic.xml | 2 +- .../main/res/layout/call_audio_device_list_cell.xml | 2 +- 5 files changed, 27 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 4d39e2b3fc..e1a4d110b1 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -139,8 +139,14 @@ class CoreContext ) } - Log.i("$TAG Reloading sound devices in 500ms") - postOnCoreThreadDelayed({ core.reloadSoundDevices() }, 500) + if (telecomManager.getCurrentlyFollowedCalls() <= 0) { + Log.i("$TAG No call found in Telecom's CallsManager, reloading sound devices in 500ms") + postOnCoreThreadDelayed({ core.reloadSoundDevices() }, 500) + } else { + Log.i( + "$TAG At least one active call in Telecom's CallsManager, let it handle the added device(s)" + ) + } } } @@ -158,7 +164,7 @@ class CoreContext postOnCoreThreadDelayed({ core.reloadSoundDevices() }, 500) } else { Log.i( - "$TAG At least one active call in Telecom's CallsManager, let it handle the removed device" + "$TAG At least one active call in Telecom's CallsManager, let it handle the removed device(s)" ) } } diff --git a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt index c4051cc7d4..b564bd2cc5 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt @@ -127,12 +127,12 @@ class TelecomCallControlCallback( Log.i("$TAG New available audio endpoints list") if (availableEndpoints != list) { Log.i( - "$TAG List size of available audio endpoints has changed, reload sound devices in SDK" + "$TAG List size of available audio endpoints has changed, reload sound devices in SDK in 500ms" ) - coreContext.postOnCoreThread { core -> + coreContext.postOnCoreThreadDelayed({ core -> core.reloadSoundDevices() Log.i("$TAG Sound devices reloaded") - } + }, 500) } availableEndpoints = list @@ -167,6 +167,7 @@ class TelecomCallControlCallback( } CallEndpointCompat.Companion.TYPE_BLUETOOTH -> { route.add(AudioDevice.Type.Bluetooth) + route.add(AudioDevice.Type.HearingAid) } CallEndpointCompat.Companion.TYPE_WIRED_HEADSET -> { route.add(AudioDevice.Type.Headphones) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 2c94289fc0..7eebbb963e 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -111,6 +111,8 @@ class CurrentCallViewModel val isHeadsetEnabled = MutableLiveData() + val isHearingAidEnabled = MutableLiveData() + val isBluetoothEnabled = MutableLiveData() val fullScreenMode = MutableLiveData() @@ -278,6 +280,7 @@ class CurrentCallViewModel updateEncryption() } + @WorkerThread override fun onAuthenticationTokenVerified(call: Call, verified: Boolean) { Log.w( "$TAG Notified that authentication token is [${if (verified) "verified" else "not verified!"}]" @@ -291,11 +294,13 @@ class CurrentCallViewModel updateAvatarModelSecurityLevel(verified) } + @WorkerThread override fun onRemoteRecording(call: Call, recording: Boolean) { Log.i("$TAG Remote recording changed: $recording") isRemoteRecordingEvent.postValue(Event(Pair(recording, displayedName.value.orEmpty()))) } + @WorkerThread override fun onStatsUpdated(call: Call, stats: CallStats) { callStatsModel.update(call, stats) } @@ -414,6 +419,7 @@ class CurrentCallViewModel } private val coreListener = object : CoreListenerStub() { + @WorkerThread override fun onCallStateChanged( core: Core, call: Call, @@ -495,6 +501,11 @@ class CurrentCallViewModel unreadMessagesCount.postValue(0) } } + + @WorkerThread + override fun onAudioDevicesListUpdated(core: Core) { + Log.i("$TAG Audio devices list has been updated") + } } @WorkerThread @@ -1235,6 +1246,7 @@ class CurrentCallViewModel isHeadsetEnabled.postValue( audioDevice?.type == AudioDevice.Type.Headphones || audioDevice?.type == AudioDevice.Type.Headset ) + isHearingAidEnabled.postValue(audioDevice?.type == AudioDevice.Type.HearingAid) isBluetoothEnabled.postValue(audioDevice?.type == AudioDevice.Type.Bluetooth) updateProximitySensor() diff --git a/app/src/main/res/layout/call_actions_generic.xml b/app/src/main/res/layout/call_actions_generic.xml index a7eb507d94..3b854ad900 100644 --- a/app/src/main/res/layout/call_actions_generic.xml +++ b/app/src/main/res/layout/call_actions_generic.xml @@ -101,7 +101,7 @@ android:layout_height="@dimen/call_button_size" android:layout_marginEnd="16dp" android:padding="@dimen/call_button_icon_padding" - android:src="@{viewModel.isHeadsetEnabled ? @drawable/headset : viewModel.isBluetoothEnabled ? @drawable/bluetooth : viewModel.isSpeakerEnabled ? @drawable/speaker_high : @drawable/speaker_slash, default=@drawable/speaker_slash}" + android:src="@{viewModel.isHearingAidEnabled ? @drawable/ear : viewModel.isHeadsetEnabled ? @drawable/headset : viewModel.isBluetoothEnabled ? @drawable/bluetooth : viewModel.isSpeakerEnabled ? @drawable/speaker_high : @drawable/speaker_slash, default=@drawable/speaker_slash}" android:background="@drawable/in_call_button_background_red" android:contentDescription="@string/content_description_change_output_audio_device" app:tint="@color/in_call_button_tint_color" diff --git a/app/src/main/res/layout/call_audio_device_list_cell.xml b/app/src/main/res/layout/call_audio_device_list_cell.xml index 4474c0c7a7..866c9e9f94 100644 --- a/app/src/main/res/layout/call_audio_device_list_cell.xml +++ b/app/src/main/res/layout/call_audio_device_list_cell.xml @@ -22,7 +22,7 @@ android:textColor="@color/in_call_label_color" android:gravity="center_vertical" android:layout_marginBottom="1dp" - android:drawableEnd="@{model.type == Type.Speaker ? @drawable/speaker_high : model.type == Type.Bluetooth || model.type == Type.HearingAid ? @drawable/bluetooth : model.type == Type.Headphones || model.type == Type.Headset ? @drawable/headset : @drawable/ear, default=@drawable/speaker_high}" + android:drawableEnd="@{model.type == Type.HearingAid ? @drawable/ear : model.type == Type.Speaker ? @drawable/speaker_high : model.type == Type.Bluetooth ? @drawable/bluetooth : model.type == Type.Headphones || model.type == Type.Headset ? @drawable/headset : @drawable/speaker_slash, default=@drawable/speaker_high}" android:drawableTint="@color/in_call_label_color" android:checked="@{model.isCurrentlySelected}" app:useMaterialThemeColors="false" From e16e767d5ae762c9487e273aa6ee7551c09c8bba Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 18 Mar 2025 12:34:22 +0100 Subject: [PATCH 031/593] Fixed wrong deleted notification channel ID... --- .../java/org/linphone/notifications/NotificationsManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 107c731842..100a25867b 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -546,7 +546,7 @@ class NotificationsManager } } else { try { - val oldId = context.getString(R.string.notification_channel_without_ringtone_incoming_call_id) + val oldId = context.getString(R.string.notification_channel_incoming_call_id) val oldChannel = notificationManager.getNotificationChannel(oldId) if (oldChannel != null) { Log.i("$TAG Deleting notification channel ID [$oldId]") From 08412ef99a47d236749908ac9894b0cccd0fdf71 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 18 Mar 2025 12:51:25 +0100 Subject: [PATCH 032/593] Prevent some crashes seen on Play Store console --- .../main/java/org/linphone/telecom/TelecomManager.kt | 10 ++++++++-- .../linphone/ui/assistant/fragment/RegisterFragment.kt | 4 +++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/linphone/telecom/TelecomManager.kt b/app/src/main/java/org/linphone/telecom/TelecomManager.kt index 785d431824..f71b43fe83 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomManager.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomManager.kt @@ -185,8 +185,14 @@ class TelecomManager } } } - } catch (e: CallException) { - Log.e("$TAG Failed to add call to Telecom's CallsManager: $e") + } catch (ce: CallException) { + Log.e("$TAG Failed to add call to Telecom's CallsManager: $ce") + } catch (se: SecurityException) { + Log.e("$TAG Security exception trying to add call to Telecom's CallsManager: $se") + } catch (ise: IllegalArgumentException) { + Log.e("$TAG Illegal argument exception trying to add call to Telecom's CallsManager: $ise") + } catch (e: Exception) { + Log.e("$TAG Exception trying to add call to Telecom's CallsManager: $e") } } } diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt index 4a0d49cd13..8cfee0ff28 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt @@ -186,8 +186,10 @@ class RegisterFragment : GenericFragment() { val telephonyManager = requireContext().getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager val countryIso = telephonyManager.networkCountryIso coreContext.postOnCoreThread { + val fragmentContext = context ?: return@postOnCoreThread + val adapter = object : ArrayAdapter( - requireContext(), + fragmentContext, R.layout.drop_down_item, viewModel.dialPlansLabelList ) { From 488a0fd98cfdb82e304348318d18a921b578c634 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 18 Mar 2025 16:43:14 +0100 Subject: [PATCH 033/593] Added advanced setting letting user choose whether to record in MKV or SMFF --- .../java/org/linphone/core/CorePreferences.kt | 7 +++ .../RecordingMediaPlayerViewModel.kt | 4 ++ .../settings/viewmodel/SettingsViewModel.kt | 11 +++++ .../java/org/linphone/utils/LinphoneUtils.kt | 11 ++++- .../res/layout/recording_player_fragment.xml | 4 +- .../res/layout/settings_advanced_fragment.xml | 46 ++++++++++++++++++- app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 8 files changed, 82 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 4b2bdb07b8..b959ba8f0c 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -119,6 +119,13 @@ class CorePreferences config.setBool("app", "route_audio_to_speaker_when_video_enabled", value) } + @get:WorkerThread @set:WorkerThread + var callRecordingUseSmffFormat: Boolean + get() = config.getBool("app", "use_smff_for_call_recording", false) + set(value) { + config.setBool("app", "use_smff_for_call_recording", value) + } + @get:WorkerThread @set:WorkerThread var automaticallyStartCallRecording: Boolean get() = config.getBool("app", "auto_start_call_record", false) diff --git a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt index 4d4bfa87b6..b025be7698 100644 --- a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt @@ -38,6 +38,7 @@ import org.linphone.core.tools.Log import org.linphone.ui.GenericViewModel import org.linphone.ui.main.recordings.model.RecordingModel import org.linphone.utils.AudioUtils +import org.linphone.utils.LinphoneUtils class RecordingMediaPlayerViewModel @UiThread @@ -56,6 +57,8 @@ class RecordingMediaPlayerViewModel val position = MutableLiveData() + val isUsingSmffFileFormat = MutableLiveData() + private var audioFocusRequest: AudioFocusRequestCompat? = null private val playerListener = PlayerListener { @@ -88,6 +91,7 @@ class RecordingMediaPlayerViewModel recordingModel = model coreContext.postOnCoreThread { core -> + isUsingSmffFileFormat.postValue(model.filePath.endsWith(LinphoneUtils.RECORDING_SMFF_FILE_EXTENSION)) initPlayer() } } diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 1da989a840..aab34bdbb1 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -197,6 +197,7 @@ class SettingsViewModel // Advanced settings val startAtBoot = MutableLiveData() val keepAliveThirdPartyAccountsService = MutableLiveData() + val useSmffForCallRecording = MutableLiveData() val deviceName = MutableLiveData() val fileSharingServerUrl = MutableLiveData() @@ -300,6 +301,7 @@ class SettingsViewModel videoFecEnabled.postValue(core.isFecEnabled) vibrateDuringIncomingCall.postValue(core.isVibrationOnIncomingCallEnabled) autoRecordCalls.postValue(corePreferences.automaticallyStartCallRecording) + useSmffForCallRecording.postValue(corePreferences.callRecordingUseSmffFormat) useWifiOnly.postValue(core.isWifiOnlyEnabled) allowIpv6.postValue(core.isIpv6Enabled) @@ -428,6 +430,15 @@ class SettingsViewModel } } + @UiThread + fun toggleUseSmffForCallRecording() { + val newValue = useSmffForCallRecording.value == false + coreContext.postOnCoreThread { core -> + corePreferences.callRecordingUseSmffFormat = newValue + useSmffForCallRecording.postValue(newValue) + } + } + @UiThread fun toggleVibrateOnIncomingCalls() { val newValue = vibrateDuringIncomingCall.value == false diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 5ee6aae335..1b6249dd8c 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -61,7 +61,8 @@ class LinphoneUtils { const val RECORDING_FILE_NAME_HEADER = "call_recording_" const val RECORDING_FILE_NAME_URI_TIMESTAMP_SEPARATOR = "_on_" - const val RECORDING_FILE_EXTENSION = ".smff" + const val RECORDING_MKV_FILE_EXTENSION = ".mkv" + const val RECORDING_SMFF_FILE_EXTENSION = ".smff" @WorkerThread fun getDefaultAccount(): Account? { @@ -451,7 +452,13 @@ class LinphoneUtils { @WorkerThread fun getRecordingFilePathForAddress(address: Address): String { - val fileName = "${RECORDING_FILE_NAME_HEADER}${address.asStringUriOnly()}${RECORDING_FILE_NAME_URI_TIMESTAMP_SEPARATOR}${System.currentTimeMillis()}$RECORDING_FILE_EXTENSION" + val extension = if (corePreferences.callRecordingUseSmffFormat) { + RECORDING_SMFF_FILE_EXTENSION + } else { + RECORDING_MKV_FILE_EXTENSION + } + Log.i("$TAG Using [$extension] file format for call recording") + val fileName = "${RECORDING_FILE_NAME_HEADER}${address.asStringUriOnly()}${RECORDING_FILE_NAME_URI_TIMESTAMP_SEPARATOR}${System.currentTimeMillis()}$extension" return FileUtils.getFileStoragePath(fileName, isRecording = true).absolutePath } diff --git a/app/src/main/res/layout/recording_player_fragment.xml b/app/src/main/res/layout/recording_player_fragment.xml index 49717b335b..3db49e36aa 100644 --- a/app/src/main/res/layout/recording_player_fragment.xml +++ b/app/src/main/res/layout/recording_player_fragment.xml @@ -162,7 +162,7 @@ android:padding="15dp" android:src="@drawable/share_network" android:contentDescription="@string/content_description_share_file" - android:visibility="gone" + android:visibility="@{viewModel.isUsingSmffFileFormat ? View.GONE : View.VISIBLE}" app:tint="@color/gray_main2_500" app:layout_constraintEnd_toStartOf="@id/save" app:layout_constraintTop_toTopOf="parent" /> @@ -176,7 +176,7 @@ android:padding="15dp" android:src="@drawable/download_simple" android:contentDescription="@string/content_description_save_file" - android:visibility="gone" + android:visibility="@{viewModel.isUsingSmffFileFormat ? View.GONE : View.VISIBLE}" app:tint="@color/gray_main2_500" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/layout/settings_advanced_fragment.xml b/app/src/main/res/layout/settings_advanced_fragment.xml index 2e73f35550..c250f9221a 100644 --- a/app/src/main/res/layout/settings_advanced_fragment.xml +++ b/app/src/main/res/layout/settings_advanced_fragment.xml @@ -145,6 +145,50 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/keep_alive_service_switch" /> + + + + + + + app:layout_constraintTop_toBottomOf="@id/use_smff_call_recording_switch"/> Nom de l\'appareil Caractères alpha-numériques uniquement URL du serveur de partage de fichier + Enregistrer les appels vidéos utilisant H265/AV1 + Utilisera un format de fichier propriétaire Chiffrement du média Rendre le chiffrement du média obligatoire Créer en mode chiffré de bout en bout les réunions et les appels de groupe diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6abd1cfae3..f39509dd24 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -307,6 +307,8 @@ Device ID Alpha-numerical characters only File sharing server URL + Record video calls using H265/AV1 + Will use a proprietary file format Media encryption Media encryption mandatory Create end-to-end encrypted meetings & group calls From 1942ee8f85f6d99365e12c71727a864b752b19e6 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 18 Mar 2025 14:00:22 +0100 Subject: [PATCH 034/593] Few tweaks trying to prevent jni global ref table overflow --- .../org/linphone/contacts/ContactLoader.kt | 32 ++++------- .../java/org/linphone/core/CorePreferences.kt | 4 ++ .../main/contacts/model/ContactAvatarModel.kt | 2 +- .../viewmodel/ContactNewOrEditViewModel.kt | 57 ++++++++----------- .../contacts/viewmodel/ContactViewModel.kt | 2 +- .../viewmodel/ContactsListViewModel.kt | 5 +- .../viewmodel/AddressSelectionViewModel.kt | 3 +- .../org/linphone/utils/PhoneNumberUtils.kt | 13 ----- 8 files changed, 47 insertions(+), 71 deletions(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactLoader.kt b/app/src/main/java/org/linphone/contacts/ContactLoader.kt index 7c430f73f5..7cdff06a20 100644 --- a/app/src/main/java/org/linphone/contacts/ContactLoader.kt +++ b/app/src/main/java/org/linphone/contacts/ContactLoader.kt @@ -219,14 +219,9 @@ class ContactLoader : LoaderManager.LoaderCallbacks { } if (!number.isNullOrEmpty()) { - if (friend.phoneNumbersWithLabel.find { - PhoneNumberUtils.arePhoneNumberWeakEqual(it.phoneNumber, number) - } == null - ) { - val phoneNumber = Factory.instance() - .createFriendPhoneNumber(number, label) - friend.addPhoneNumberWithLabel(phoneNumber) - } + val phoneNumber = Factory.instance() + .createFriendPhoneNumber(number, label) + friend.addPhoneNumberWithLabel(phoneNumber) } } ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE -> { @@ -250,17 +245,14 @@ class ContactLoader : LoaderManager.LoaderCallbacks { } } ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> { - val vCard = friend.vcard - if (vCard != null) { - val givenName: String? = cursor.getString(givenNameColumn) - if (!givenName.isNullOrEmpty()) { - vCard.givenName = givenName - } + val givenName: String? = cursor.getString(givenNameColumn) + if (!givenName.isNullOrEmpty()) { + friend.firstName = givenName + } - val familyName: String? = cursor.getString(familyNameColumn) - if (!familyName.isNullOrEmpty()) { - vCard.familyName = familyName - } + val familyName: String? = cursor.getString(familyNameColumn) + if (!familyName.isNullOrEmpty()) { + friend.lastName = familyName } } } @@ -291,7 +283,7 @@ class ContactLoader : LoaderManager.LoaderCallbacks { if (core.globalState == GlobalState.Shutdown || core.globalState == GlobalState.Off) { Log.w("$TAG Core is being stopped or already destroyed, abort") - } else if (friends.isEmpty) { + } else if (friends.isEmpty()) { Log.w("$TAG No friend created!") } else { Log.i("$TAG ${friends.size} friends fetched") @@ -322,7 +314,7 @@ class ContactLoader : LoaderManager.LoaderCallbacks { friends.remove(localFriend.refKey) localFriend.nativeUri = newlyFetchedFriend.nativeUri // Native URI isn't stored in linphone database, needs to be updated - if (newlyFetchedFriend.vcard?.asVcard4String() == localFriend.vcard?.asVcard4String()) continue + if (newlyFetchedFriend.dumpVcard() == localFriend.dumpVcard()) continue localFriend.edit() // Update basic fields that may have changed diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index b959ba8f0c..f373b45f4d 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -311,6 +311,10 @@ class CorePreferences val hideAssistantThirdPartySipAccount: Boolean get() = config.getBool("ui", "assistant_hide_third_party_account", false) + @get:WorkerThread + val magicSearchResultsLimit: Int + get() = config.getInt("ui", "max_number_of_magic_search_results", 1000) + @get:WorkerThread val singleSignOnClientId: String get() = config.getString("app", "oidc_client_id", "linphone")!! diff --git a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt index 6e85704400..70edd275fa 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt @@ -161,7 +161,7 @@ class ContactAvatarModel @WorkerThread fun getNameToUseForSorting(): String? { val sortByFirstName = corePreferences.sortContactsByFirstName - val firstOrLastName = if (sortByFirstName) friend.vcard?.givenName else friend.vcard?.familyName + val firstOrLastName = if (sortByFirstName) friend.firstName else friend.lastName return firstOrLastName ?: friend.name ?: friend.organization ?: friend.vcard?.fullName } diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt index 05aecb53ad..c50f21f05c 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt @@ -97,13 +97,8 @@ class ContactNewOrEditViewModel if (exists) { Log.i("$TAG Found friend [${friend.name}] using ref key [$refKey]") - val vCard = friend.vcard - if (vCard != null) { - firstName.postValue(vCard.givenName) - lastName.postValue(vCard.familyName) - } else { - // TODO: What to do if vCard is null? - } + firstName.postValue(friend.firstName.orEmpty()) + lastName.postValue(friend.lastName.orEmpty()) id.postValue(friend.refKey ?: friend.vcard?.uid) @@ -169,33 +164,29 @@ class ContactNewOrEditViewModel friend.edit() friend.name = name - - val vCard = friend.vcard - if (vCard != null) { - vCard.givenName = fn - vCard.familyName = ln - - val picture = picturePath.value.orEmpty() - if (picture.isNotEmpty()) { - if (picture.contains(TEMP_PICTURE_NAME)) { - val newFile = FileUtils.getFileStoragePath( - getPictureFileName(), - isImage = true, - overrideExisting = true - ) - val oldFile = FileUtils.getProperFilePath(picture).toUri() - viewModelScope.launch { - FileUtils.copyFile(oldFile, newFile) - } - val newPicture = FileUtils.getProperFilePath(newFile.absolutePath) - Log.i("$TAG Temporary picture [$picture] copied to [$newPicture]") - friend.photo = newPicture - } else { - friend.photo = FileUtils.getProperFilePath(picture) + friend.firstName = fn + friend.lastName = ln + + val picture = picturePath.value.orEmpty() + if (picture.isNotEmpty()) { + if (picture.contains(TEMP_PICTURE_NAME)) { + val newFile = FileUtils.getFileStoragePath( + getPictureFileName(), + isImage = true, + overrideExisting = true + ) + val oldFile = FileUtils.getProperFilePath(picture).toUri() + viewModelScope.launch { + FileUtils.copyFile(oldFile, newFile) } + val newPicture = FileUtils.getProperFilePath(newFile.absolutePath) + Log.i("$TAG Temporary picture [$picture] copied to [$newPicture]") + friend.photo = newPicture } else { - friend.photo = null + friend.photo = FileUtils.getProperFilePath(picture) } + } else { + friend.photo = null } friend.organization = organization @@ -327,8 +318,8 @@ class ContactNewOrEditViewModel @UiThread fun isPendingChanges(): Boolean { if (isEdit.value == true) { - if (firstName.value.orEmpty() != friend.vcard?.givenName.orEmpty()) return true - if (lastName.value.orEmpty() != friend.vcard?.familyName.orEmpty()) return true + if (firstName.value.orEmpty() != friend.firstName.orEmpty()) return true + if (lastName.value.orEmpty() != friend.lastName.orEmpty()) return true if (picturePath.value.orEmpty() != friend.photo.orEmpty()) return true if (company.value.orEmpty() != friend.organization.orEmpty()) return true if (jobTitle.value.orEmpty() != friend.jobTitle.orEmpty()) return true diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt index 7cbb7090e2..a4a3c5c406 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt @@ -369,7 +369,7 @@ class ContactViewModel fun exportContactAsVCard() { coreContext.postOnCoreThread { if (::friend.isInitialized) { - val vCard = friend.vcard?.asVcard4String() + val vCard = friend.dumpVcard() if (!vCard.isNullOrEmpty()) { Log.i("$TAG Friend has been successfully dumped as vCard string") val fileName = friend.name.orEmpty().replace(" ", "_").lowercase( diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt index efddee3ce1..c0396b1a4a 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt @@ -109,7 +109,8 @@ class ContactsListViewModel coreContext.contactsManager.addListener(contactsListener) magicSearch = core.createMagicSearch() - magicSearch.limitedSearch = false + magicSearch.limitedSearch = true + magicSearch.searchLimit = corePreferences.magicSearchResultsLimit magicSearch.addListener(magicSearchListener) coreContext.postOnMainThread { @@ -197,7 +198,7 @@ class ContactsListViewModel @UiThread fun exportContactAsVCard(friend: Friend) { coreContext.postOnCoreThread { - val vCard = friend.vcard?.asVcard4String() + val vCard = friend.dumpVcard() if (!vCard.isNullOrEmpty()) { Log.i("$TAG Friend has been successfully dumped as vCard string") val fileName = friend.name.orEmpty().replace(" ", "_").lowercase( diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt index fae45f0144..c095029ba7 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt @@ -104,7 +104,8 @@ abstract class AddressSelectionViewModel coreContext.contactsManager.addListener(contactsListener) magicSearch = core.createMagicSearch() - magicSearch.limitedSearch = false + magicSearch.limitedSearch = true + magicSearch.searchLimit = corePreferences.magicSearchResultsLimit magicSearch.addListener(magicSearchListener) } diff --git a/app/src/main/java/org/linphone/utils/PhoneNumberUtils.kt b/app/src/main/java/org/linphone/utils/PhoneNumberUtils.kt index cf7e760013..47715ae50c 100644 --- a/app/src/main/java/org/linphone/utils/PhoneNumberUtils.kt +++ b/app/src/main/java/org/linphone/utils/PhoneNumberUtils.kt @@ -117,18 +117,5 @@ class PhoneNumberUtils { else -> ContactsContract.CommonDataKinds.BaseTypes.TYPE_CUSTOM } } - - @AnyThread - fun arePhoneNumberWeakEqual(number1: String, number2: String): Boolean { - return trimPhoneNumber(number1) == trimPhoneNumber(number2) - } - - @AnyThread - private fun trimPhoneNumber(phoneNumber: String): String { - return phoneNumber.replace(" ", "") - .replace("-", "") - .replace("(", "") - .replace(")", "") - } } } From 2eb376fd2ddd25ec389dd7ed0f6e1014d8c0185b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 19 Mar 2025 13:58:12 +0100 Subject: [PATCH 035/593] Prevent schedule meeting button being pressed multiple times --- .../main/meetings/viewmodel/ScheduleMeetingViewModel.kt | 8 +++++--- app/src/main/res/layout/meeting_edit_fragment.xml | 1 + app/src/main/res/layout/meeting_schedule_fragment.xml | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/ScheduleMeetingViewModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/ScheduleMeetingViewModel.kt index 160ffd51af..1302689fc8 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/ScheduleMeetingViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/ScheduleMeetingViewModel.kt @@ -373,11 +373,13 @@ class ScheduleMeetingViewModel @UiThread fun schedule() { + operationInProgress.value = true if (subject.value.orEmpty().isEmpty() || participants.value.orEmpty().isEmpty()) { Log.e( "$TAG Either no subject was set or no participant was selected, can't schedule meeting." ) showRedToast(R.string.meeting_schedule_mandatory_field_not_filled_toast, R.drawable.warning_circle) + operationInProgress.value = false return } @@ -385,7 +387,6 @@ class ScheduleMeetingViewModel Log.i( "$TAG Scheduling ${if (isBroadcastSelected.value == true) "broadcast" else "meeting"}" ) - operationInProgress.postValue(true) val localAccount = core.defaultAccount val localAddress = localAccount?.params?.identityAddress @@ -452,17 +453,18 @@ class ScheduleMeetingViewModel @UiThread fun update() { + operationInProgress.value = true + coreContext.postOnCoreThread { core -> Log.i( "$TAG Updating ${if (isBroadcastSelected.value == true) "broadcast" else "meeting"}" ) if (!::conferenceInfo.isInitialized) { Log.e("No conference info to edit found!") + operationInProgress.postValue(false) return@postOnCoreThread } - operationInProgress.postValue(true) - conferenceInfo.subject = subject.value conferenceInfo.description = description.value diff --git a/app/src/main/res/layout/meeting_edit_fragment.xml b/app/src/main/res/layout/meeting_edit_fragment.xml index 972fc5b511..21800ace36 100644 --- a/app/src/main/res/layout/meeting_edit_fragment.xml +++ b/app/src/main/res/layout/meeting_edit_fragment.xml @@ -397,6 +397,7 @@ android:layout_gravity="end|bottom" android:layout_margin="16dp" android:src="@drawable/check" + android:enabled="@{!viewModel.operationInProgress}" android:contentDescription="@string/content_description_meeting_schedule" app:tint="?attr/color_on_main" app:backgroundTint="?attr/color_main1_500" diff --git a/app/src/main/res/layout/meeting_schedule_fragment.xml b/app/src/main/res/layout/meeting_schedule_fragment.xml index 09edb393ff..55b8b3573b 100644 --- a/app/src/main/res/layout/meeting_schedule_fragment.xml +++ b/app/src/main/res/layout/meeting_schedule_fragment.xml @@ -543,6 +543,7 @@ android:layout_gravity="end|bottom" android:layout_margin="16dp" android:src="@drawable/check" + android:enabled="@{!viewModel.operationInProgress}" android:contentDescription="@string/content_description_meeting_schedule" app:tint="?attr/color_on_main" app:backgroundTint="?attr/color_main1_500" From a0d74c803651b6abe84d7328b4ccf78b4711a734 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 20 Mar 2025 12:26:44 +0100 Subject: [PATCH 036/593] Added back apply prefix to phone numbers for calls & chat setting --- .../viewmodel/AccountSettingsViewModel.kt | 5 ++++ .../res/layout/account_settings_fragment.xml | 29 ++++++++++++++++++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt index bf9ef2f4f4..4ee95049f5 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt @@ -90,6 +90,7 @@ class AccountSettingsViewModel val mwiUri = MutableLiveData() val voicemailUri = MutableLiveData() + val applyPrefix = MutableLiveData() val replacePlusBy00 = MutableLiveData() val cpimInBasicChatRooms = MutableLiveData() @@ -167,6 +168,8 @@ class AccountSettingsViewModel mwiUri.postValue(params.mwiServerAddress?.asStringUriOnly().orEmpty()) voicemailUri.postValue(params.voicemailAddress?.asStringUriOnly().orEmpty()) + + applyPrefix.postValue(params.useInternationalPrefixForCallsAndChats) replacePlusBy00.postValue(params.isDialEscapePlusEnabled) expire.postValue(params.expires.toString()) @@ -301,6 +304,8 @@ class AccountSettingsViewModel newParams.ccmpServerUrl = ccmpServerUrl.value newParams.limeServerUrl = limeServerUrl.value + + newParams.useInternationalPrefixForCallsAndChats = applyPrefix.value == true newParams.isDialEscapePlusEnabled = replacePlusBy00.value == true account.params = newParams diff --git a/app/src/main/res/layout/account_settings_fragment.xml b/app/src/main/res/layout/account_settings_fragment.xml index 904c344828..4370b094d7 100644 --- a/app/src/main/res/layout/account_settings_fragment.xml +++ b/app/src/main/res/layout/account_settings_fragment.xml @@ -193,6 +193,33 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/mwi_uri_title" + app:layout_constraintBottom_toTopOf="@id/apply_international_switch"/> + + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b0ee0e5e79..7c87c7e1bc 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -334,6 +334,7 @@ Utiliser CPIM dans les conversations \"basiques\" URI de la messagerie vocale URI du serveur MWI (Message Waiting Indicator) + Formatter les numéros en utilisant l\'indicatif international Remplacer + par 00 lors du formattage des numéros de téléphone Mettre à jour le mot de passe diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f39509dd24..5adcd07ff2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -374,6 +374,7 @@ Use CPIM in \"basic\" conversations Voicemail URI MWI server URI (Message Waiting Indicator) + Format phone numbers using international prefix Replace + by 00 when formatting phone numbers Update password From 2abad0ab9a4cef2d04ce7ff2dc109140bc7422ed Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 20 Mar 2025 13:00:10 +0100 Subject: [PATCH 037/593] Fetch call history from Core instead of Account if only one of them --- .../ui/main/history/viewmodel/HistoryListViewModel.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryListViewModel.kt b/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryListViewModel.kt index c5fd7304c1..873736ee9b 100644 --- a/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryListViewModel.kt @@ -142,7 +142,13 @@ class HistoryListViewModel var count = 0 val account = LinphoneUtils.getDefaultAccount() - val logs = account?.callLogs ?: coreContext.core.callLogs + // Fetch all call logs if only one account to workaround no history issue + val logs = if (coreContext.core.accountList.size > 1) { + account?.callLogs ?: coreContext.core.callLogs + } else { + coreContext.core.callLogs + } + for (callLog in logs) { val model = CallLogModel(callLog) if (isCallLogMatchingFilter(model, filter)) { From cad90752dbed56929d78fc463f7f3822978e7048 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 20 Mar 2025 14:55:49 +0100 Subject: [PATCH 038/593] Fixed call logs details if it's not attached to any account --- .../ui/main/history/viewmodel/HistoryViewModel.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt b/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt index 9f51fd231e..05af48bdd7 100644 --- a/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt @@ -330,6 +330,16 @@ class HistoryViewModel history.add(historyModel) } + // Required when "unique account displays all call logs from Core" workaround + if (list.isEmpty()) { + for (log in coreContext.core.callLogs) { + if (log.remoteAddress.weakEqual(address)) { + val historyModel = CallLogHistoryModel(log) + history.add(historyModel) + } + } + } + Log.i("$TAG Found [${history.size}] call logs") historyCallLogs.postValue(history) } From b194272f91766244824de03b4f1508269a3d304b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 19 Mar 2025 16:43:10 +0100 Subject: [PATCH 039/593] Use newly added chatRoom.getAccount() to dynamically filter conversations --- .../viewmodel/ConversationsListViewModel.kt | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationsListViewModel.kt index 9da02a47c9..72bf6f3494 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationsListViewModel.kt @@ -174,16 +174,12 @@ class ConversationsListViewModel @WorkerThread private fun addChatRoom(chatRoom: ChatRoom) { - val localAddress = chatRoom.localAddress - val peerAddress = chatRoom.peerAddress - + val identifier = chatRoom.identifier + val chatRoomAccount = chatRoom.account val defaultAccount = LinphoneUtils.getDefaultAccount() - if (defaultAccount == null || - defaultAccount.params.identityAddress?.weakEqual(localAddress) == false - ) - { + if (defaultAccount == null || chatRoomAccount == null || chatRoomAccount != defaultAccount) { Log.w( - "$TAG Chat room with local address [${localAddress.asStringUriOnly()}] and peer address [${peerAddress.asStringUriOnly()}] was created but not displaying it because it doesn't belong to currently default account" + "$TAG Chat room with identifier [$identifier] was created but not displaying it because it doesn't belong to currently default account" ) return } @@ -191,16 +187,16 @@ class ConversationsListViewModel val hideEmptyChatRooms = coreContext.core.config.getBool("misc", "hide_empty_chat_rooms", true) // Hide empty chat rooms only applies to 1-1 conversations if (hideEmptyChatRooms && !LinphoneUtils.isChatRoomAGroup(chatRoom) && chatRoom.lastMessageInHistory == null) { - Log.w("$TAG Chat room with local address [${localAddress.asStringUriOnly()}] and peer address [${peerAddress.asStringUriOnly()}] is empty, not adding it to match Core setting") + Log.w("$TAG Chat room with identifier [$identifier] is empty, not adding it to match Core setting") return } val currentList = conversations.value.orEmpty() val found = currentList.find { - it.chatRoom.peerAddress.weakEqual(peerAddress) + it.chatRoom.identifier == identifier } if (found != null) { - Log.w("$TAG Created chat room with local address [${localAddress.asStringUriOnly()}] and peer address [${peerAddress.asStringUriOnly()}] is already in the list, skipping") + Log.w("$TAG Created chat room with identifier [$identifier] is already in the list, skipping") return } @@ -216,27 +212,27 @@ class ConversationsListViewModel val model = ConversationModel(chatRoom) newList.add(model) newList.addAll(currentList) - Log.i("$TAG Adding chat room with local address [${localAddress.asStringUriOnly()}] and peer address [${peerAddress.asStringUriOnly()}] to list") + Log.i("$TAG Adding chat room with identifier [$identifier] to list") conversations.postValue(newList) } @WorkerThread private fun removeChatRoom(chatRoom: ChatRoom) { val currentList = conversations.value.orEmpty() - val peerAddress = chatRoom.peerAddress + val identifier = chatRoom.identifier val found = currentList.find { - it.chatRoom.peerAddress.weakEqual(peerAddress) + it.chatRoom.identifier == identifier } if (found != null) { val newList = arrayListOf() newList.addAll(currentList) newList.remove(found) found.destroy() - Log.i("$TAG Removing chat room [${peerAddress.asStringUriOnly()}] from list") + Log.i("$TAG Removing chat room with identifier [$identifier] from list") conversations.postValue(newList) } else { Log.w( - "$TAG Failed to find item in list matching deleted chat room peer address [${peerAddress.asStringUriOnly()}]" + "$TAG Failed to find item in list matching deleted chat room identifier [$identifier]" ) } From fecf067b50c8cc494cf04023c9da82dd875bbb08 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 20 Mar 2025 21:24:14 +0100 Subject: [PATCH 040/593] Fixed wrong setting disabled when VFS is enabled --- app/src/main/res/layout/settings_chat.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/settings_chat.xml b/app/src/main/res/layout/settings_chat.xml index 0e95227443..cf87e4e8e4 100644 --- a/app/src/main/res/layout/settings_chat.xml +++ b/app/src/main/res/layout/settings_chat.xml @@ -40,7 +40,6 @@ android:layout_height="wrap_content" android:layout_marginTop="20dp" android:layout_marginEnd="16dp" - android:enabled="@{!viewModel.isVfsEnabled}" android:checked="@{viewModel.autoDownloadEnabled}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -69,6 +68,7 @@ android:layout_height="wrap_content" android:layout_marginTop="20dp" android:layout_marginEnd="16dp" + android:enabled="@{!viewModel.isVfsEnabled}" android:checked="@{viewModel.autoExportMediaToNativeGallery}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/auto_download_switch" /> From a83f9d4424da50112f40a7415ed30298127d708b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 20 Mar 2025 21:37:19 +0100 Subject: [PATCH 041/593] Moved call related advanced settings to dedicated sub-section, added back auto answer --- .../java/org/linphone/core/CoreContext.kt | 15 + .../java/org/linphone/core/CorePreferences.kt | 14 + .../notifications/NotificationsManager.kt | 9 +- .../telecom/TelecomCallControlCallback.kt | 35 +- .../fragment/SettingsAdvancedFragment.kt | 6 +- .../settings/viewmodel/SettingsViewModel.kt | 41 ++- .../res/layout/settings_advanced_calls.xml | 320 ++++++++++++++++++ .../res/layout/settings_advanced_fragment.xml | 271 ++------------- app/src/main/res/values-fr/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 10 files changed, 454 insertions(+), 263 deletions(-) create mode 100644 app/src/main/res/layout/settings_advanced_calls.xml diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index e1a4d110b1..c50e81b9f4 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -265,6 +265,21 @@ class CoreContext "$TAG Call [${call.remoteAddress.asStringUriOnly()}] state changed [$currentState]" ) when (currentState) { + Call.State.IncomingReceived -> { + if (corePreferences.autoAnswerEnabled) { + val autoAnswerDelay = corePreferences.autoAnswerDelay + if (autoAnswerDelay == 0) { + Log.w("$TAG Auto answering call immediately") + answerCall(call) + } else { + Log.i("$TAG Scheduling auto answering in $autoAnswerDelay milliseconds") + postOnCoreThreadDelayed({ + Log.w("$TAG Auto answering call") + answerCall(call) + }, autoAnswerDelay.toLong()) + } + } + } Call.State.OutgoingInit -> { val conferenceInfo = core.findConferenceInformationFromUri(call.remoteAddress) // Do not show outgoing call view for conference calls, wait for connected state diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index f373b45f4d..5e05581f02 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -154,6 +154,20 @@ class CorePreferences config.setBool("misc", "real_early_media", value) } + @get:WorkerThread @set:WorkerThread + var autoAnswerEnabled: Boolean + get() = config.getBool("app", "auto_answer", false) + set(value) { + config.setBool("app", "auto_answer", value) + } + + @get:WorkerThread @set:WorkerThread + var autoAnswerDelay: Int + get() = config.getInt("app", "auto_answer_delay", 0) + set(value) { + config.setInt("app", "auto_answer_delay", value) + } + // Conversation related @get:WorkerThread @set:WorkerThread diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 100a25867b..b90ebf8fea 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -189,8 +189,9 @@ class NotificationsManager state: Call.State?, message: String ) { - Log.i("$TAG Call state changed: [$state]") - when (state) { + val currentState = call.state + Log.i("$TAG Call state changed: [$currentState]") + when (currentState) { Call.State.IncomingReceived, Call.State.IncomingEarlyMedia -> { Log.i( "$TAG Showing incoming call notification for [${call.remoteAddress.asStringUriOnly()}]" @@ -205,14 +206,14 @@ class NotificationsManager } Call.State.Connected, Call.State.StreamsRunning -> { - if (call.state == Call.State.Connected && call.dir == Call.Dir.Incoming) { + if (currentState == Call.State.Connected && call.dir == Call.Dir.Incoming) { Log.i( "$TAG Connected call was incoming (so it was answered), removing incoming call notification" ) removeIncomingCallNotification() } - if (call.state == Call.State.Connected || call.dir == Call.Dir.Incoming) { + if (currentState == Call.State.Connected || call.dir == Call.Dir.Incoming) { Log.i( "$TAG Showing connected call notification for [${call.remoteAddress.asStringUriOnly()}]" ) diff --git a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt index b564bd2cc5..4f71adcaad 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt @@ -60,21 +60,7 @@ class TelecomCallControlCallback( Log.i("$TAG Call [${call.remoteAddress.asStringUriOnly()}] state changed [$state]") if (state == Call.State.Connected) { if (call.dir == Call.Dir.Incoming) { - val isVideo = LinphoneUtils.isVideoEnabled(call) - val type = if (isVideo) { - CallAttributesCompat.Companion.CALL_TYPE_VIDEO_CALL - } else { - CallAttributesCompat.Companion.CALL_TYPE_AUDIO_CALL - } - scope.launch { - Log.i("$TAG Answering [${if (isVideo) "video" else "audio"}] call") - callControl.answer(type) - } - - if (isVideo && corePreferences.routeAudioToSpeakerWhenVideoIsEnabled) { - Log.i("$TAG Answering video call, routing audio to speaker") - AudioUtils.routeAudioToSpeaker(call) - } + answerCall() } else { scope.launch { Log.i("$TAG Setting call active") @@ -116,6 +102,7 @@ class TelecomCallControlCallback( val state = call.state Log.i("$TAG Call state currently is [$state]") when (state) { + Call.State.Connected, Call.State.StreamsRunning -> answerCall() Call.State.End -> callEnded() Call.State.Error -> callError("") Call.State.Released -> callEnded() @@ -283,6 +270,24 @@ class TelecomCallControlCallback( return false } + private fun answerCall() { + val isVideo = LinphoneUtils.isVideoEnabled(call) + val type = if (isVideo) { + CallAttributesCompat.Companion.CALL_TYPE_VIDEO_CALL + } else { + CallAttributesCompat.Companion.CALL_TYPE_AUDIO_CALL + } + scope.launch { + Log.i("$TAG Answering [${if (isVideo) "video" else "audio"}] call") + callControl.answer(type) + } + + if (isVideo && corePreferences.routeAudioToSpeakerWhenVideoIsEnabled) { + Log.i("$TAG Answering video call, routing audio to speaker") + AudioUtils.routeAudioToSpeaker(call) + } + } + private fun callEnded() { val reason = call.reason val direction = call.dir diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedFragment.kt index 733b7360c2..bcd6c96aeb 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedFragment.kt @@ -131,9 +131,9 @@ class SettingsAdvancedFragment : GenericMainFragment() { viewModel.mediaEncryptionLabels ) adapter.setDropDownViewResource(R.layout.generic_dropdown_cell) - binding.mediaEncryption.adapter = adapter - binding.mediaEncryption.onItemSelectedListener = mediaEncryptionDropdownListener - binding.mediaEncryption.setSelection(index) + binding.advancedCallsSettings.mediaEncryption.adapter = adapter + binding.advancedCallsSettings.mediaEncryption.onItemSelectedListener = mediaEncryptionDropdownListener + binding.advancedCallsSettings.mediaEncryption.setSelection(index) } private fun setupInputAudioDevicePicker() { diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index aab34bdbb1..5fc1444c1d 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -203,6 +203,8 @@ class SettingsViewModel val fileSharingServerUrl = MutableLiveData() val remoteProvisioningUrl = MutableLiveData() + val expandAdvancedCalls = MutableLiveData() + val mediaEncryptionIndex = MutableLiveData() val mediaEncryptionLabels = arrayListOf() private val mediaEncryptionValues = arrayListOf() @@ -210,6 +212,8 @@ class SettingsViewModel val createEndToEndEncryptedConferences = MutableLiveData() val acceptEarlyMedia = MutableLiveData() val allowOutgoingEarlyMedia = MutableLiveData() + val autoAnswerIncomingCalls = MutableLiveData() + val autoAnswerIncomingCallsDelay = MutableLiveData() val expandAudioDevices = MutableLiveData() val inputAudioDeviceIndex = MutableLiveData() @@ -262,6 +266,7 @@ class SettingsViewModel expandNetwork.value = false expandUserInterface.value = false expandTunnel.value = false + expandAdvancedCalls.value = false expandAudioDevices.value = false expandAudioCodecs.value = false expandVideoCodecs.value = false @@ -331,6 +336,12 @@ class SettingsViewModel fileSharingServerUrl.postValue(core.fileTransferServer) remoteProvisioningUrl.postValue(core.provisioningUri) + createEndToEndEncryptedConferences.postValue(corePreferences.createEndToEndEncryptedMeetingsAndGroupCalls) + acceptEarlyMedia.postValue(corePreferences.acceptEarlyMedia) + allowOutgoingEarlyMedia.postValue(corePreferences.allowOutgoingEarlyMedia) + autoAnswerIncomingCalls.postValue(corePreferences.autoAnswerEnabled) + autoAnswerIncomingCallsDelay.postValue(corePreferences.autoAnswerDelay) + setupMediaEncryption() setupAudioDevices() setupCodecs() @@ -763,9 +774,6 @@ class SettingsViewModel } mediaEncryptionMandatory.postValue(core.isMediaEncryptionMandatory) - createEndToEndEncryptedConferences.postValue(corePreferences.createEndToEndEncryptedMeetingsAndGroupCalls) - acceptEarlyMedia.postValue(corePreferences.acceptEarlyMedia) - allowOutgoingEarlyMedia.postValue(corePreferences.allowOutgoingEarlyMedia) } @UiThread @@ -821,6 +829,28 @@ class SettingsViewModel } } + @UiThread + fun toggleEnableAutoAnswerIncomingCalls() { + val newValue = autoAnswerIncomingCalls.value == false + + coreContext.postOnCoreThread { core -> + corePreferences.autoAnswerEnabled = newValue + autoAnswerIncomingCalls.postValue(newValue) + } + } + + @UiThread + fun updateAutoAnswerIncomingCallsDelay(newValue: String) { + if (newValue.isNotEmpty()) { + try { + val delay = newValue.toInt() + corePreferences.autoAnswerDelay = delay + } catch (nfe: NumberFormatException) { + Log.e("$TAG Ignoring new auto answer incoming calls delay as it can't be converted to int: $nfe") + } + } + } + @UiThread fun updateDeviceName() { coreContext.postOnCoreThread { @@ -874,6 +904,11 @@ class SettingsViewModel } } + @UiThread + fun toggleAdvancedCallsExpand() { + expandAdvancedCalls.value = expandAdvancedCalls.value == false + } + @UiThread fun toggleAudioDevicesExpand() { expandAudioDevices.value = expandAudioDevices.value == false diff --git a/app/src/main/res/layout/settings_advanced_calls.xml b/app/src/main/res/layout/settings_advanced_calls.xml new file mode 100644 index 0000000000..d65c942314 --- /dev/null +++ b/app/src/main/res/layout/settings_advanced_calls.xml @@ -0,0 +1,320 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/settings_advanced_fragment.xml b/app/src/main/res/layout/settings_advanced_fragment.xml index c250f9221a..e7a30bf90e 100644 --- a/app/src/main/res/layout/settings_advanced_fragment.xml +++ b/app/src/main/res/layout/settings_advanced_fragment.xml @@ -1,6 +1,7 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:bind="http://schemas.android.com/tools"> @@ -114,241 +115,6 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/start_at_boot_switch"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + app:layout_constraintTop_toBottomOf="@id/keep_alive_service_switch"/> + + + + + app:layout_constraintTop_toBottomOf="@id/advanced_calls_settings"/> Créer en mode chiffré de bout en bout les réunions et les appels de groupe Accepter l\'early media Autoriser l\'early media pour les appels sortants + Décrocher automatiquement les appels entrants + Délai avant le décrochage automatique + Délai en millisecondes URL de configuration distante Télécharger & appliquer Périphériques audio diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5adcd07ff2..2cb537fa05 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -314,6 +314,9 @@ Create end-to-end encrypted meetings & group calls Accept early media Allow outgoing early media + Auto answer incoming calls + Delay before auto answering call + Delay in milliseconds Remote provisioning URL Download & apply Audio devices From 1255d626af48fdd5de13def79216bc80b6483ca1 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 21 Mar 2025 09:03:19 +0100 Subject: [PATCH 042/593] Fixed recordings order, now most recent on the top --- .../ui/main/recordings/viewmodel/RecordingsListViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingsListViewModel.kt index ad44756ea2..0b88792df8 100644 --- a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingsListViewModel.kt @@ -118,7 +118,7 @@ class RecordingsListViewModel } } - list.sortBy { + list.sortByDescending { it.timestamp } recordings.postValue(list) From 2f9eb2f0ab5211ca91e09e83a6709bc8e10dbc76 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 21 Mar 2025 11:13:01 +0100 Subject: [PATCH 043/593] Improved message when WiFi only mode is enabled & active network isn't Wifi nor Ethernet --- .../org/linphone/ui/main/viewmodel/MainViewModel.kt | 11 ++++++++++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt index b107cd8fb7..83c0077cc0 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt @@ -43,6 +43,7 @@ import org.linphone.core.GlobalState import org.linphone.core.MessageWaitingIndication import org.linphone.core.RegistrationState import org.linphone.core.VFS +import org.linphone.core.tools.AndroidPlatformHelper import org.linphone.core.tools.Log import org.linphone.utils.AppUtils import org.linphone.utils.Event @@ -582,7 +583,15 @@ class MainViewModel val reachable = coreContext.core.isNetworkReachable Log.i("$TAG Network is ${if (reachable) "reachable" else "not reachable"}") if (!reachable && coreContext.core.globalState == GlobalState.On) { - val label = AppUtils.getString(R.string.network_not_reachable) + val label = if (coreContext.core.isWifiOnlyEnabled) { + if (AndroidPlatformHelper.isReady() && AndroidPlatformHelper.instance().isActiveNetworkWifiOnlyCompliant) { + AppUtils.getString(R.string.network_not_reachable) + } else { + AppUtils.getString(R.string.network_is_not_wifi) + } + } else { + AppUtils.getString(R.string.network_not_reachable) + } addAlert(NETWORK_NOT_REACHABLE, label) } else { removeAlert(NETWORK_NOT_REACHABLE) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6c2da25243..199f902955 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -783,6 +783,7 @@ %s notifications en attente Vous n\'êtes pas connecté à internet + Mode Wi-Fi uniquement activé Opération en cours, merci de patienter… Conversations Contacts diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2cb537fa05..e18b0e5606 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -824,6 +824,7 @@ %s notifications for other account(s) You aren\'t connected to internet + Wi-Fi only mode enabled Operation in progress, please wait Conversations Contacts From 2aed404167daf042366c0c97164ef6e9eec8f84a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 21 Mar 2025 15:41:33 +0100 Subject: [PATCH 044/593] Have automaticallyShowDialpad setting also work on new call/transfer call screens while already in call --- .../ui/call/fragment/NewCallFragment.kt | 30 +++++++++++++++++-- .../ui/call/fragment/TransferCallFragment.kt | 26 ++++++++++++++-- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/call/fragment/NewCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/NewCallFragment.kt index b44437249d..660fbee3fd 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/NewCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/NewCallFragment.kt @@ -32,6 +32,7 @@ import androidx.navigation.navGraphViewModels import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.bottomsheet.BottomSheetBehavior import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers import org.linphone.core.Address @@ -61,6 +62,16 @@ class NewCallFragment : GenericCallFragment() { R.id.call_nav_graph ) + private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_COLLAPSED || newState == BottomSheetBehavior.STATE_HIDDEN) { + viewModel.isNumpadVisible.value = false + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) { } + } + private lateinit var adapter: ConversationsContactsAndSuggestionsListAdapter private val listener = object : ContactNumberOrAddressClickListener { @@ -185,12 +196,15 @@ class NewCallFragment : GenericCallFragment() { } } + val bottomSheetBehavior = BottomSheetBehavior.from(binding.numpadLayout.root) + bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback) + viewModel.isNumpadVisible.observe(viewLifecycleOwner) { visible -> - val standardBottomSheetBehavior = BottomSheetBehavior.from(binding.numpadLayout.root) if (visible) { - standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED } else { - standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED } } @@ -201,6 +215,16 @@ class NewCallFragment : GenericCallFragment() { } } + override fun onResume() { + super.onResume() + + coreContext.postOnCoreThread { + if (corePreferences.automaticallyShowDialpad) { + viewModel.isNumpadVisible.postValue(true) + } + } + } + override fun onPause() { super.onPause() diff --git a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt index fa086dde0e..a46075aef3 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt @@ -33,6 +33,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.bottomsheet.BottomSheetBehavior import kotlin.getValue import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.core.tools.Log import org.linphone.databinding.CallTransferFragmentBinding @@ -63,6 +64,16 @@ class TransferCallFragment : GenericCallFragment() { R.id.call_nav_graph ) + private val bottomSheetCallback = object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_COLLAPSED || newState == BottomSheetBehavior.STATE_HIDDEN) { + viewModel.isNumpadVisible.value = false + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) { } + } + private lateinit var callViewModel: CurrentCallViewModel private lateinit var callsViewModel: CallsViewModel @@ -208,12 +219,15 @@ class TransferCallFragment : GenericCallFragment() { } } + val bottomSheetBehavior = BottomSheetBehavior.from(binding.numpadLayout.root) + bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + bottomSheetBehavior.addBottomSheetCallback(bottomSheetCallback) + viewModel.isNumpadVisible.observe(viewLifecycleOwner) { visible -> - val standardBottomSheetBehavior = BottomSheetBehavior.from(binding.numpadLayout.root) if (visible) { - standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED } else { - standardBottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED } } @@ -238,6 +252,12 @@ class TransferCallFragment : GenericCallFragment() { R.string.call_transfer_current_call_title, callViewModel.displayedName.value ?: callViewModel.displayedAddress.value ) + + coreContext.postOnCoreThread { + if (corePreferences.automaticallyShowDialpad) { + viewModel.isNumpadVisible.postValue(true) + } + } } private fun showConfirmAttendedTransferDialog(callModel: CallModel) { From 6121cb41bf46e7c8523b4be755d310a5fb3cdb0e Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Sat, 22 Mar 2025 13:33:57 +0100 Subject: [PATCH 045/593] Fixed remove all call logs when workaround is active --- .../history/viewmodel/HistoryListViewModel.kt | 16 ++++++++++------ .../main/history/viewmodel/HistoryViewModel.kt | 1 + 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryListViewModel.kt b/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryListViewModel.kt index 873736ee9b..422996ffdc 100644 --- a/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryListViewModel.kt @@ -106,13 +106,16 @@ class HistoryListViewModel @UiThread fun removeAllCallLogs() { coreContext.postOnCoreThread { core -> - val account = LinphoneUtils.getDefaultAccount() - if (account != null) { - account.clearCallLogs() - } else { - for (callLog in core.callLogs) { - core.removeCallLog(callLog) + // TODO FIXME: remove workaround later + if (coreContext.core.accountList.size > 1) { + val account = LinphoneUtils.getDefaultAccount() + if (account != null) { + account.clearCallLogs() + } else { + core.clearCallLogs() } + } else { + core.clearCallLogs() } historyDeletedEvent.postValue(Event(true)) @@ -143,6 +146,7 @@ class HistoryListViewModel val account = LinphoneUtils.getDefaultAccount() // Fetch all call logs if only one account to workaround no history issue + // TODO FIXME: remove workaround later val logs = if (coreContext.core.accountList.size > 1) { account?.callLogs ?: coreContext.core.callLogs } else { diff --git a/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt b/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt index 05af48bdd7..6f96dda670 100644 --- a/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt @@ -331,6 +331,7 @@ class HistoryViewModel } // Required when "unique account displays all call logs from Core" workaround + // TODO FIXME: remove workaround later if (list.isEmpty()) { for (log in coreContext.core.callLogs) { if (log.remoteAddress.weakEqual(address)) { From faac4111d9592a9e43404aebeaba18c5543c3e45 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 24 Mar 2025 08:58:28 +0100 Subject: [PATCH 046/593] Updated CHANGELOG from release/6.0 branch --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4307142eaa..788833e9d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,27 @@ Group changes to describe their impact on the project, as follows: Fixed for any bug fixes. Security to invite users to upgrade in case of vulnerabilities. +## [6.0.1] - 2025-03-21 + +### Added +- Start at boot & auto answer settings added back +- Interface setting to have dialpad automatically opened in start call view +- Replace "+" by "00" and do not apply prefix for calls & chat account settings +- Setting to let user choose whether to record calls using MKV or SMFF format (the later allows to record H265/AV1 video but is a proprietary file format that can't be read outside of Linphone) + +### Changed +- Reverted the way of playing incoming call ringone (you may have to configure your own ringtone again), was causing various issues depending on devices/firmwares +- Show all call history entries if only one account is configured (workaround for missing history for now until a proper fix will be done in SDK) + +### Fixed +- Issue preventing bluetooth Hearing Aids from working properly (and fixed earpiece/hearing aids icon) +- Prevent Qr Code scanner to use static picture camera +- Prevent user from connecting the same account multiple times +- Quit menu visibility not updated when changing Keep Alive setting +- Participant selection in group when typing "@" +- Recordings order has been reversed to have newest ones at top +- Improved message when network is not reachable due to "Wifi only mode" being enabled +- Various crash & bug fixes ## [6.0.0] - 2025-03-11 From 50bd8f67d5ab30aca89a03da8927408f3e75fb09 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 24 Mar 2025 09:44:47 +0100 Subject: [PATCH 047/593] Check if lateinit conference was initialized --- .../viewmodel/ConferenceViewModel.kt | 197 ++++++++++-------- 1 file changed, 106 insertions(+), 91 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt index 65d4c76e91..e7834fbb59 100644 --- a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt @@ -366,15 +366,17 @@ class ConferenceViewModel @UiThread fun goToConversation() { - coreContext.postOnCoreThread { core -> - Log.i("$TAG Navigating to conference's conversation") - val chatRoom = conference.chatRoom - if (chatRoom != null) { - goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom))) - } else { - Log.e( - "$TAG No chat room available for current conference [${conference.conferenceAddress?.asStringUriOnly()}]" - ) + if (::conference.isInitialized) { + coreContext.postOnCoreThread { core -> + Log.i("$TAG Navigating to conference's conversation") + val chatRoom = conference.chatRoom + if (chatRoom != null) { + goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom))) + } else { + Log.e( + "$TAG No chat room available for current conference [${conference.conferenceAddress?.asStringUriOnly()}]" + ) + } } } } @@ -393,82 +395,93 @@ class ConferenceViewModel @UiThread fun inviteSipUrisIntoConference(uris: List) { - coreContext.postOnCoreThread { core -> - val addresses = arrayListOf
() - for (uri in uris) { - val address = core.interpretUrl(uri, false) - if (address != null) { - addresses.add(address) - Log.i("$TAG Address [${address.asStringUriOnly()}] will be added to conference") - } else { - Log.e( - "$TAG Failed to parse SIP URI [$uri] into address, can't add it to the conference!" - ) - showRedToast(R.string.conference_failed_to_add_participant_invalid_address_toast, R.drawable.warning_circle) + if (::conference.isInitialized) { + coreContext.postOnCoreThread { core -> + val addresses = arrayListOf
() + for (uri in uris) { + val address = core.interpretUrl(uri, false) + if (address != null) { + addresses.add(address) + Log.i("$TAG Address [${address.asStringUriOnly()}] will be added to conference") + } else { + Log.e( + "$TAG Failed to parse SIP URI [$uri] into address, can't add it to the conference!" + ) + showRedToast( + R.string.conference_failed_to_add_participant_invalid_address_toast, + R.drawable.warning_circle + ) + } } + val addressesArray = arrayOfNulls
(addresses.size) + addresses.toArray(addressesArray) + Log.i("$TAG Trying to add [${addressesArray.size}] new participant(s) into conference") + conference.addParticipants(addressesArray) } - val addressesArray = arrayOfNulls
(addresses.size) - addresses.toArray(addressesArray) - Log.i("$TAG Trying to add [${addressesArray.size}] new participant(s) into conference") - conference.addParticipants(addressesArray) } } @WorkerThread fun kickParticipant(participant: Participant) { - coreContext.postOnCoreThread { - Log.i( - "$TAG Kicking participant [${participant.address.asStringUriOnly()}] out of conference" - ) - conference.removeParticipant(participant) + if (::conference.isInitialized) { + coreContext.postOnCoreThread { + Log.i( + "$TAG Kicking participant [${participant.address.asStringUriOnly()}] out of conference" + ) + conference.removeParticipant(participant) + } } } @WorkerThread fun setNewLayout(newLayout: Int) { - val call = conference.call - if (call != null) { - val params = call.core.createCallParams(call) - if (params != null) { - val currentLayout = getCurrentLayout(call) - if (currentLayout != newLayout) { - when (newLayout) { - AUDIO_ONLY_LAYOUT -> { - Log.i("$TAG Changing conference layout to [Audio Only]") - params.isVideoEnabled = false - } - ACTIVE_SPEAKER_LAYOUT -> { - Log.i("$TAG Changing conference layout to [Active Speaker]") - params.conferenceVideoLayout = Conference.Layout.ActiveSpeaker + if (::conference.isInitialized) { + val call = conference.call + if (call != null) { + val params = call.core.createCallParams(call) + if (params != null) { + val currentLayout = getCurrentLayout(call) + if (currentLayout != newLayout) { + when (newLayout) { + AUDIO_ONLY_LAYOUT -> { + Log.i("$TAG Changing conference layout to [Audio Only]") + params.isVideoEnabled = false + } + + ACTIVE_SPEAKER_LAYOUT -> { + Log.i("$TAG Changing conference layout to [Active Speaker]") + params.conferenceVideoLayout = Conference.Layout.ActiveSpeaker + } + + GRID_LAYOUT -> { + Log.i("$TAG Changing conference layout to [Grid]") + params.conferenceVideoLayout = Conference.Layout.Grid + } } - GRID_LAYOUT -> { - Log.i("$TAG Changing conference layout to [Grid]") - params.conferenceVideoLayout = Conference.Layout.Grid + + if (currentLayout == AUDIO_ONLY_LAYOUT) { + // Previous layout was audio only, make sure video isn't sent without user consent when switching layout + Log.i( + "$TAG Previous layout was [Audio Only], enabling video but in receive only direction" + ) + params.isVideoEnabled = true + params.videoDirection = MediaDirection.RecvOnly } - } - if (currentLayout == AUDIO_ONLY_LAYOUT) { - // Previous layout was audio only, make sure video isn't sent without user consent when switching layout - Log.i( - "$TAG Previous layout was [Audio Only], enabling video but in receive only direction" + Log.i("$TAG Updating conference's call params") + call.update(params) + conferenceLayout.postValue(newLayout) + } else { + Log.w( + "$TAG The conference is already using selected layout, aborting layout change" ) - params.isVideoEnabled = true - params.videoDirection = MediaDirection.RecvOnly } - - Log.i("$TAG Updating conference's call params") - call.update(params) - conferenceLayout.postValue(newLayout) } else { - Log.w( - "$TAG The conference is already using selected layout, aborting layout change" - ) + Log.e("$TAG Failed to create call params, aborting layout change") } } else { - Log.e("$TAG Failed to create call params, aborting layout change") + Log.e("$TAG Failed to get call from conference, aborting layout change") } - } else { - Log.e("$TAG Failed to get call from conference, aborting layout change") } } @@ -691,36 +704,38 @@ class ConferenceViewModel @WorkerThread private fun addParticipant(participant: Participant) { - val list = arrayListOf() - list.addAll(participants.value.orEmpty()) + if (::conference.isInitialized) { + val list = arrayListOf() + list.addAll(participants.value.orEmpty()) - val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress( - participant.address - ) - val newModel = ConferenceParticipantModel( - participant, - avatarModel, - isMeAdmin.value == true, - false, - { participant -> // Remove from conference - removeParticipantEvent.postValue( - Event(Pair(avatarModel.name.value.orEmpty(), participant)) - ) - }, - { participant, setAdmin -> // Change admin status - conference.setParticipantAdminStatus(participant, setAdmin) - } - ) - list.add(newModel) + val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress( + participant.address + ) + val newModel = ConferenceParticipantModel( + participant, + avatarModel, + isMeAdmin.value == true, + false, + { participant -> // Remove from conference + removeParticipantEvent.postValue( + Event(Pair(avatarModel.name.value.orEmpty(), participant)) + ) + }, + { participant, setAdmin -> // Change admin status + conference.setParticipantAdminStatus(participant, setAdmin) + } + ) + list.add(newModel) - participants.postValue(sortParticipantList(list)) - participantsLabel.postValue( - AppUtils.getStringWithPlural( - R.plurals.conference_participants_list_title, - list.size, - "${list.size}" + participants.postValue(sortParticipantList(list)) + participantsLabel.postValue( + AppUtils.getStringWithPlural( + R.plurals.conference_participants_list_title, + list.size, + "${list.size}" + ) ) - ) + } } @WorkerThread From 9ce803667b6904c012c949393329ea49b4dab946 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 24 Mar 2025 09:49:34 +0100 Subject: [PATCH 048/593] Do not send meeting message invitation when chat is disabled --- .../ui/main/meetings/viewmodel/MeetingViewModel.kt | 3 ++- .../ui/main/meetings/viewmodel/MeetingsListViewModel.kt | 3 ++- .../ui/main/meetings/viewmodel/ScheduleMeetingViewModel.kt | 7 +++++-- app/src/main/res/layout/meeting_schedule_fragment.xml | 3 +++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingViewModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingViewModel.kt index 94499c278a..c0490fa0d8 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingViewModel.kt @@ -24,6 +24,7 @@ import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import java.util.TimeZone import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.core.Address import org.linphone.core.ConferenceInfo import org.linphone.core.ConferenceScheduler @@ -93,7 +94,7 @@ class MeetingViewModel "$TAG Conference ${conferenceScheduler.info?.subject} cancelled" ) val params = LinphoneUtils.getChatRoomParamsToCancelMeeting() - if (params != null) { + if (params != null && !corePreferences.disableChat) { conferenceScheduler.sendInvitations(params) } else { operationInProgress.postValue(false) diff --git a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt index d9dfdd8ce5..361f9a5e6b 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt @@ -23,6 +23,7 @@ import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.core.Account import org.linphone.core.AccountListenerStub import org.linphone.core.Address @@ -88,7 +89,7 @@ class MeetingsListViewModel "$TAG Conference ${conferenceScheduler.info?.subject} cancelled" ) val params = LinphoneUtils.getChatRoomParamsToCancelMeeting() - if (params != null) { + if (params != null && !corePreferences.disableChat) { conferenceScheduler.sendInvitations(params) } else { operationInProgress.postValue(false) diff --git a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/ScheduleMeetingViewModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/ScheduleMeetingViewModel.kt index 1302689fc8..880da234ab 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/ScheduleMeetingViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/ScheduleMeetingViewModel.kt @@ -80,6 +80,8 @@ class ScheduleMeetingViewModel val hideBroadcast = MutableLiveData() + val chatDisabled = MutableLiveData() + val conferenceCreatedEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -125,7 +127,7 @@ class ScheduleMeetingViewModel ) } - if (sendInvitations.value == true) { + if (sendInvitations.value == true && !corePreferences.disableChat) { Log.i("$TAG User asked for invitations to be sent, let's do it") val chatRoomParams = coreContext.core.createConferenceParams(null) @@ -178,10 +180,11 @@ class ScheduleMeetingViewModel init { coreContext.postOnCoreThread { hideBroadcast.postValue(corePreferences.disableBroadcasts) + chatDisabled.postValue(corePreferences.disableChat) + sendInvitations.postValue(!corePreferences.disableChat) } isBroadcastSelected.value = false showBroadcastHelp.value = false - sendInvitations.value = true selectedTimeZone.value = availableTimeZones.find { it.id == TimeZone.getDefault().id diff --git a/app/src/main/res/layout/meeting_schedule_fragment.xml b/app/src/main/res/layout/meeting_schedule_fragment.xml index 55b8b3573b..ea6205b89d 100644 --- a/app/src/main/res/layout/meeting_schedule_fragment.xml +++ b/app/src/main/res/layout/meeting_schedule_fragment.xml @@ -514,6 +514,8 @@ android:layout_marginEnd="16dp" android:textColor="@color/gray_main2_600" android:textSize="14sp" + android:visibility="@{viewModel.chatDisabled ? View.GONE : View.VISIBLE}" + android:enabled="@{!viewModel.chatDisabled}" android:checked="@={viewModel.sendInvitations}" app:layout_constraintTop_toBottomOf="@id/separator_5" app:layout_constraintStart_toStartOf="parent" /> @@ -524,6 +526,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="10dp" + android:visibility="@{viewModel.chatDisabled ? View.GONE : View.VISIBLE}" android:text="@string/meeting_schedule_send_invitations_title" app:layout_constraintStart_toEndOf="@id/send_invitations" app:layout_constraintTop_toTopOf="@id/send_invitations" From 77f61c1cfa19c0c8aa950258fb8043b0eebe48f9 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 24 Mar 2025 10:17:31 +0100 Subject: [PATCH 049/593] Updated version code to match the one on release/6.0 branch --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 76a87511e8..2cb956c5d8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -100,8 +100,8 @@ android { applicationId = packageName minSdk = 28 targetSdk = 35 - versionCode = 600000 // 6.00.000 - versionName = "6.0.0" + versionCode = 600001 // 6.00.000 + versionName = "6.0.1" manifestPlaceholders["appAuthRedirectScheme"] = packageName From fb3feb0bc34150b9739867429656cf62d7e090f5 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 24 Mar 2025 11:33:00 +0100 Subject: [PATCH 050/593] This should prevent crashes on Androids < 13 due to broken POST_NOTIFICATIONS permission check --- .../org/linphone/compatibility/Api33Compatibility.kt | 8 ++++++++ .../java/org/linphone/compatibility/Compatibility.kt | 7 +++++++ .../org/linphone/core/CoreFileTransferService.kt | 12 ++++-------- .../linphone/notifications/NotificationsManager.kt | 8 +++----- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/linphone/compatibility/Api33Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api33Compatibility.kt index 0206980736..57f228df24 100644 --- a/app/src/main/java/org/linphone/compatibility/Api33Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api33Compatibility.kt @@ -20,6 +20,8 @@ package org.linphone.compatibility import android.Manifest +import android.content.Context +import android.content.pm.PackageManager import android.os.Build import androidx.annotation.RequiresApi @@ -34,5 +36,11 @@ class Api33Compatibility { Manifest.permission.CAMERA ) } + + fun isPostNotificationsPermissionGranted(context: Context): Boolean { + return context.checkSelfPermission( + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + } } } diff --git a/app/src/main/java/org/linphone/compatibility/Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Compatibility.kt index c4cc48734d..9d828fb550 100644 --- a/app/src/main/java/org/linphone/compatibility/Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Compatibility.kt @@ -111,6 +111,13 @@ class Compatibility { return false } + fun isPostNotificationsPermissionGranted(context: Context): Boolean { + if (Version.sdkAboveOrEqual(Version.API33_ANDROID_13_TIRAMISU)) { + return Api33Compatibility.isPostNotificationsPermissionGranted(context) + } + return true + } + fun enterPipMode(activity: Activity): Boolean { if (Version.sdkStrictlyBelow(Version.API31_ANDROID_12)) { return Api28Compatibility.enterPipMode(activity) diff --git a/app/src/main/java/org/linphone/core/CoreFileTransferService.kt b/app/src/main/java/org/linphone/core/CoreFileTransferService.kt index 1d57cf0ae7..be58b3d09d 100644 --- a/app/src/main/java/org/linphone/core/CoreFileTransferService.kt +++ b/app/src/main/java/org/linphone/core/CoreFileTransferService.kt @@ -19,19 +19,18 @@ */ package org.linphone.core -import android.Manifest +import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Intent -import android.content.pm.PackageManager import android.os.IBinder import androidx.annotation.AnyThread import androidx.annotation.MainThread import androidx.annotation.WorkerThread -import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R +import org.linphone.compatibility.Compatibility import org.linphone.core.tools.Log import org.linphone.core.tools.service.FileTransferService import org.linphone.ui.main.MainActivity @@ -171,14 +170,11 @@ class CoreFileTransferService : FileTransferService() { postNotification() } + @SuppressLint("MissingPermission") @AnyThread private fun postNotification() { val notificationsManager = NotificationManagerCompat.from(this) - if (ActivityCompat.checkSelfPermission( - this, - Manifest.permission.POST_NOTIFICATIONS - ) == PackageManager.PERMISSION_GRANTED - ) { + if (Compatibility.isPostNotificationsPermissionGranted(this)) { if (mServiceNotification != null) { Log.i("$TAG Sending notification to manager") notificationsManager.notify(SERVICE_NOTIF_ID, mServiceNotification) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index b90ebf8fea..149d21cca6 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -20,6 +20,7 @@ package org.linphone.notifications import android.Manifest +import android.annotation.SuppressLint import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager @@ -1022,13 +1023,10 @@ class NotificationsManager notify(notifiable.notificationId, notification, CHAT_TAG) } + @SuppressLint("MissingPermission") @WorkerThread private fun notify(id: Int, notification: Notification, tag: String? = null) { - if (ActivityCompat.checkSelfPermission( - context, - Manifest.permission.POST_NOTIFICATIONS - ) == PackageManager.PERMISSION_GRANTED - ) { + if (Compatibility.isPostNotificationsPermissionGranted(context)) { Log.i( "$TAG Notifying using ID [$id] and ${if (tag == null) "without tag" else "with tag [$tag]"}" ) From c6fa645f94b91066acfcae590bf309beb9d55243 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 24 Mar 2025 11:39:03 +0100 Subject: [PATCH 051/593] Fixed invisible conference notification icon --- .../org/linphone/notifications/NotificationsManager.kt | 2 +- .../main/res/drawable/video_conference_notification.xml | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/drawable/video_conference_notification.xml diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 149d21cca6..4cd86eec8d 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -1154,7 +1154,7 @@ class NotificationsManager val isVideo = LinphoneUtils.isVideoEnabled(call) val smallIcon = if (isConference) { - R.drawable.video_conference + R.drawable.video_conference_notification } else if (isVideo) { R.drawable.video_camera } else { diff --git a/app/src/main/res/drawable/video_conference_notification.xml b/app/src/main/res/drawable/video_conference_notification.xml new file mode 100644 index 0000000000..22d115f50b --- /dev/null +++ b/app/src/main/res/drawable/video_conference_notification.xml @@ -0,0 +1,9 @@ + + + From b22ab7024e998898ea25c9138ab911b26aaf0413 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 24 Mar 2025 13:34:10 +0100 Subject: [PATCH 052/593] Fixed contact lookup if phone number starts by 00 instead of + --- app/src/main/java/org/linphone/contacts/ContactsManager.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 1f9afffa16..291fc6516e 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -32,6 +32,7 @@ import androidx.annotation.WorkerThread import androidx.core.app.ActivityCompat import androidx.core.app.Person import androidx.core.graphics.drawable.IconCompat +import androidx.core.text.isDigitsOnly import androidx.loader.app.LoaderManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -454,7 +455,7 @@ class ContactsManager sipUri } - return if (!username.isNullOrEmpty() && username.startsWith("+")) { + return if (!username.isNullOrEmpty() && (username.startsWith("+") || username.isDigitsOnly())) { Log.d("$TAG Looking for friend with phone number [$username]") val foundUsingPhoneNumber = coreContext.core.findFriendByPhoneNumber(username) if (foundUsingPhoneNumber != null) { @@ -516,7 +517,7 @@ class ContactsManager model } else { Log.d("$TAG Looking for friend matching SIP URI [$key]") - val friend = coreContext.contactsManager.findContactByAddress(clone) + val friend = findContactByAddress(clone) if (friend != null) { Log.d("$TAG Matching friend [${friend.name}] found for SIP URI [$key]") val model = ContactAvatarModel(friend, address) From 6767bc09f980c8c87cad223c62b46c68688b56fa Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 24 Mar 2025 13:36:16 +0100 Subject: [PATCH 053/593] Fixed displayed SIP URI in call history details in case we find a matching contact that has a different SIP URI in addition to the one used for the call --- .../java/org/linphone/ui/main/history/model/CallLogModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/history/model/CallLogModel.kt b/app/src/main/java/org/linphone/ui/main/history/model/CallLogModel.kt index 33e24fdfd7..2179a07c29 100644 --- a/app/src/main/java/org/linphone/ui/main/history/model/CallLogModel.kt +++ b/app/src/main/java/org/linphone/ui/main/history/model/CallLogModel.kt @@ -98,9 +98,9 @@ class CallLogModel friendExists = coreContext.contactsManager.isContactAvailable(friend) } displayedAddress = if (corePreferences.onlyDisplaySipUriUsername) { - avatarModel.friend.address?.username ?: address.username ?: "" + address.username ?: "" } else { - avatarModel.friend.address?.asStringUriOnly() ?: address.asStringUriOnly() + sipUri } iconResId = LinphoneUtils.getCallIconResId(callLog.status, callLog.dir) From 10f2d7cd788c824398c86dfcf70ff4e916acac6c Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 24 Mar 2025 14:03:43 +0100 Subject: [PATCH 054/593] Prevent today indicator in meetings list from blinking upon refresh --- .../linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt b/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt index 8208d31efc..8153737769 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt @@ -170,6 +170,7 @@ class MeetingsListAdapter : private class MeetingDiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: MeetingListItemModel, newItem: MeetingListItemModel): Boolean { + if (oldItem.isTodayIndicator && newItem.isTodayIndicator) return true if (oldItem.model is MeetingModel && newItem.model is MeetingModel) { return oldItem.model.id.isNotEmpty() && oldItem.model.id == newItem.model.id } @@ -180,6 +181,7 @@ class MeetingsListAdapter : oldItem: MeetingListItemModel, newItem: MeetingListItemModel ): Boolean { + if (oldItem.isTodayIndicator && newItem.isTodayIndicator) return true if (oldItem.model is MeetingModel && newItem.model is MeetingModel) { return oldItem.model.subject.value.orEmpty().isNotEmpty() && oldItem.model.subject.value == newItem.model.subject.value && From d6494cd27c74b0dc1c208f36428d592f9855bdad Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 24 Mar 2025 17:09:20 +0100 Subject: [PATCH 055/593] Ask for full screen intent if not granted --- .../java/org/linphone/ui/main/MainActivity.kt | 27 ++++++++++- .../ui/main/viewmodel/MainViewModel.kt | 45 ++++++++++++------- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 56 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/MainActivity.kt b/app/src/main/java/org/linphone/ui/main/MainActivity.kt index 99f96af86c..f8effd852c 100644 --- a/app/src/main/java/org/linphone/ui/main/MainActivity.kt +++ b/app/src/main/java/org/linphone/ui/main/MainActivity.kt @@ -120,12 +120,23 @@ class MainActivity : GenericActivity() { ) { isGranted -> if (isGranted) { Log.i("$TAG POST_NOTIFICATIONS permission has been granted") - viewModel.updatePostNotificationsPermission() + viewModel.updateMissingPermissionAlert() } else { Log.w("$TAG POST_NOTIFICATIONS permission has been denied!") } } + private val fullScreenIntentPermissionLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + Log.i("$TAG USE_FULL_SCREEN_INTENT permission has been granted") + viewModel.updateMissingPermissionAlert() + } else { + Log.w("$TAG USE_FULL_SCREEN_INTENT permission has been denied!") + } + } + @SuppressLint("InlinedApi") override fun onCreate(savedInstanceState: Bundle?) { // Must be done before the setContentView @@ -204,6 +215,18 @@ class MainActivity : GenericActivity() { } } + viewModel.askFullScreenIntentPermissionEvent.observe(this) { + it.consume { + if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.USE_FULL_SCREEN_INTENT)) { + Log.w("$TAG Asking for USE_FULL_SCREEN_INTENT permission") + fullScreenIntentPermissionLauncher.launch(Manifest.permission.USE_FULL_SCREEN_INTENT) + } else { + Log.i("$TAG Permission request for USE_FULL_SCREEN_INTENT will be automatically denied, go to manage app full screen intent android settings instead") + Compatibility.requestFullScreenIntentPermission(this) + } + } + } + viewModel.defaultAccountRegistrationErrorEvent.observe(this) { it.consume { error -> val tag = "DEFAULT_ACCOUNT_REGISTRATION_ERROR" @@ -397,7 +420,7 @@ class MainActivity : GenericActivity() { viewModel.enableAccountMonitoring(true) viewModel.checkForNewAccount() viewModel.updateNetworkReachability() - viewModel.updatePostNotificationsPermission() + viewModel.updateMissingPermissionAlert() } override fun onNewIntent(intent: Intent) { diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt index 83c0077cc0..b6ff5e1665 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt @@ -19,13 +19,10 @@ */ package org.linphone.ui.main.viewmodel -import android.Manifest -import android.content.pm.PackageManager import android.os.Build import androidx.annotation.RequiresApi import androidx.annotation.UiThread import androidx.annotation.WorkerThread -import androidx.core.content.ContextCompat import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -33,6 +30,7 @@ import kotlinx.coroutines.launch import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R +import org.linphone.compatibility.Compatibility import org.linphone.core.Account import org.linphone.core.Call import org.linphone.core.ChatMessage @@ -60,6 +58,7 @@ class MainViewModel const val MWI_MESSAGES_WAITING = 4 const val NON_DEFAULT_ACCOUNT_NOTIFICATIONS = 5 const val NON_DEFAULT_ACCOUNT_NOT_CONNECTED = 10 + const val FULL_SCREEN_INTENTS_PERMISSION_NOT_GRANTED = 16 const val SEND_NOTIFICATIONS_PERMISSION_NOT_GRANTED = 17 const val NETWORK_NOT_REACHABLE = 19 const val SINGLE_CALL = 20 @@ -94,6 +93,10 @@ class MainViewModel MutableLiveData>() } + val askFullScreenIntentPermissionEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + val showNewAccountToastEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -350,7 +353,7 @@ class MainViewModel } } - updatePostNotificationsPermission() + updateMissingPermissionAlert() if (VFS.isEnabled(coreContext.context)) { val cache = corePreferences.vfsCachePath @@ -383,8 +386,13 @@ class MainViewModel } @UiThread - fun updatePostNotificationsPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + fun updateMissingPermissionAlert() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + coreContext.postOnCoreThread { + checkFullScreenIntentNotificationPermission() + checkPostNotificationsPermission() + } + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { coreContext.postOnCoreThread { checkPostNotificationsPermission() } @@ -418,7 +426,9 @@ class MainViewModel fun onTopBarClicked() { if (atLeastOneCall.value == true) { goBackToCallEvent.value = Event(true) - } else if (!isPostNotificationsPermissionGranted()) { + } else if (!Compatibility.hasFullScreenIntentPermission(coreContext.context)) { + askFullScreenIntentPermissionEvent.value = Event(true) + } else if (!Compatibility.isPostNotificationsPermissionGranted(coreContext.context)) { askPostNotificationsPermissionEvent.value = Event(true) } else { openDrawerEvent.value = Event(true) @@ -554,7 +564,7 @@ class MainViewModel NETWORK_NOT_REACHABLE -> { alertIcon.postValue(R.drawable.wifi_slash) } - SEND_NOTIFICATIONS_PERMISSION_NOT_GRANTED -> { + SEND_NOTIFICATIONS_PERMISSION_NOT_GRANTED, FULL_SCREEN_INTENTS_PERMISSION_NOT_GRANTED -> { alertIcon.postValue(R.drawable.bell_simple_slash) } SINGLE_CALL, MULTIPLE_CALLS -> { @@ -598,21 +608,24 @@ class MainViewModel } } - private fun isPostNotificationsPermissionGranted(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - ContextCompat.checkSelfPermission( - coreContext.context, - Manifest.permission.POST_NOTIFICATIONS - ) == PackageManager.PERMISSION_GRANTED + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + @WorkerThread + private fun checkFullScreenIntentNotificationPermission() { + if (!Compatibility.hasFullScreenIntentPermission(coreContext.context)) { + Log.w("$TAG USE_FULL_SCREEN_INTENT seems to be not granted!") + val label = AppUtils.getString(R.string.full_screen_intent_permission_not_granted) + coreContext.postOnCoreThread { + addAlert(FULL_SCREEN_INTENTS_PERMISSION_NOT_GRANTED, label) + } } else { - true + removeAlert(FULL_SCREEN_INTENTS_PERMISSION_NOT_GRANTED) } } @RequiresApi(Build.VERSION_CODES.TIRAMISU) @WorkerThread private fun checkPostNotificationsPermission() { - if (!isPostNotificationsPermissionGranted()) { + if (!Compatibility.isPostNotificationsPermissionGranted(coreContext.context)) { Log.w("$TAG POST_NOTIFICATIONS seems to be not granted!") val label = AppUtils.getString(R.string.post_notifications_permission_not_granted) coreContext.postOnCoreThread { diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 199f902955..736e51e0c4 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -790,6 +790,7 @@ Favoris Suggestions La permission de poster des notifications n\'est pas donnée ! + La permission d\'afficher les appels entrants n\'est pas donnée ! %s message vocal en attente %s messages vocaux en attente diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e18b0e5606..492ac6b3b3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -831,6 +831,7 @@ Favorites Suggestions Post notifications permission not granted! + Show incoming call permission not granted! %s new voice message %s new voice messages From 7018cd34428507c0d8a647ebf0f8b4da322e868e Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 24 Mar 2025 23:41:18 +0100 Subject: [PATCH 056/593] Fixed crash when default device URI is null --- .../settings/viewmodel/SettingsViewModel.kt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 5fc1444c1d..2ed4bd630c 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -90,7 +90,7 @@ class SettingsViewModel val autoRecordCalls = MutableLiveData() - val goToIncomingCallNotificationChannelSettingsEvent = MutableLiveData>() + val goToIncomingCallNotificationChannelSettingsEvent = MutableLiveData>() // Conversations settings val showConversationsSettings = MutableLiveData() @@ -471,11 +471,18 @@ class SettingsViewModel @UiThread fun changeRingtone() { coreContext.postOnCoreThread { core -> - val defaultDeviceRingtone = RingtoneManager.getActualDefaultRingtoneUri(coreContext.context, RingtoneManager.TYPE_RINGTONE) - val coreRingtone = core.ring?.toUri() - Log.i("$TAG Currently set ringtone in Core is [$coreRingtone], device default ringtone is [$defaultDeviceRingtone]") - val currentRingtone = coreRingtone ?: defaultDeviceRingtone - goToIncomingCallNotificationChannelSettingsEvent.postValue(Event(currentRingtone)) + try { + val defaultDeviceRingtone = RingtoneManager.getActualDefaultRingtoneUri( + coreContext.context, + RingtoneManager.TYPE_RINGTONE + ) + val coreRingtone = core.ring?.toUri() + Log.i("$TAG Currently set ringtone in Core is [$coreRingtone], device default ringtone is [$defaultDeviceRingtone]") + val currentRingtone = coreRingtone ?: defaultDeviceRingtone + goToIncomingCallNotificationChannelSettingsEvent.postValue(Event(currentRingtone)) + } catch (e: Exception) { + Log.e("$TAG Failed to get current ringtone: $e") + } } } From d150027c2497979bc83a725b6c3e78e34506dfca Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 25 Mar 2025 11:17:57 +0100 Subject: [PATCH 057/593] Delay heavy tasks to prevent ServiceDidNotStartInTimeException (for example) --- .../java/org/linphone/contacts/ContactLoader.kt | 8 ++++---- .../main/java/org/linphone/core/CoreContext.kt | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactLoader.kt b/app/src/main/java/org/linphone/contacts/ContactLoader.kt index 7cdff06a20..211403a597 100644 --- a/app/src/main/java/org/linphone/contacts/ContactLoader.kt +++ b/app/src/main/java/org/linphone/contacts/ContactLoader.kt @@ -107,9 +107,9 @@ class ContactLoader : LoaderManager.LoaderCallbacks { } Log.i("$TAG Load finished, found ${cursor.count} entries in cursor") - coreContext.postOnCoreThread { + coreContext.postOnCoreThreadWhenAvailableForHeavyTask({ parseFriends(cursor) - } + }, "parse friends") } @MainThread @@ -265,9 +265,9 @@ class ContactLoader : LoaderManager.LoaderCallbacks { Log.i("$TAG Contacts parsed, posting another task to handle adding them (or not)") // Re-post another task to allow other tasks on Core thread - coreContext.postOnCoreThread { + coreContext.postOnCoreThreadWhenAvailableForHeavyTask({ addFriendsIfNeeded() - } + }, "add friends to Core") } catch (sde: StaleDataException) { Log.e("$TAG State Data Exception: $sde") } catch (ise: IllegalStateException) { diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index c50e81b9f4..1cd2500b88 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -38,6 +38,7 @@ import androidx.lifecycle.MutableLiveData import com.google.firebase.crashlytics.FirebaseCrashlytics import kotlin.system.exitProcess import org.linphone.BuildConfig +import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.contacts.ContactsManager import org.linphone.core.tools.Log @@ -653,6 +654,21 @@ class CoreContext } } + @AnyThread + fun postOnCoreThreadWhenAvailableForHeavyTask(@WorkerThread lambda: (core: Core) -> Unit, name: String) { + postOnCoreThread { + if (core.callsNb >= 1) { + Log.i("$TAG At least one call is active, wait until there is no more call before executing lambda [$name] (checking again in 1 sec)") + coreContext.postOnCoreThreadDelayed({ + postOnCoreThreadWhenAvailableForHeavyTask(lambda, name) + }, 1000) + } else { + Log.i("$TAG No active call at the moment, executing lambda [$name] right now") + lambda.invoke(core) + } + } + } + @AnyThread fun postOnMainThread( @UiThread lambda: () -> Unit From 8dda38a9259474573a813dfa4e7e7d152a183eac Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 25 Mar 2025 13:41:47 +0100 Subject: [PATCH 058/593] Parse friends in a coroutine scope, no need to do it on the Core's thread --- .../org/linphone/contacts/ContactLoader.kt | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactLoader.kt b/app/src/main/java/org/linphone/contacts/ContactLoader.kt index 211403a597..5f7f6b9220 100644 --- a/app/src/main/java/org/linphone/contacts/ContactLoader.kt +++ b/app/src/main/java/org/linphone/contacts/ContactLoader.kt @@ -29,8 +29,14 @@ import androidx.annotation.WorkerThread import androidx.loader.app.LoaderManager import androidx.loader.content.CursorLoader import androidx.loader.content.Loader +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import java.lang.Exception import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Core import org.linphone.core.Factory import org.linphone.core.Friend import org.linphone.core.FriendList @@ -63,6 +69,8 @@ class ContactLoader : LoaderManager.LoaderCallbacks { private val friends = HashMap() + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + @MainThread override fun onCreateLoader(id: Int, args: Bundle?): Loader { Log.i("$TAG Creating and starting cursor loader") @@ -107,26 +115,27 @@ class ContactLoader : LoaderManager.LoaderCallbacks { } Log.i("$TAG Load finished, found ${cursor.count} entries in cursor") - coreContext.postOnCoreThreadWhenAvailableForHeavyTask({ - parseFriends(cursor) - }, "parse friends") + coreContext.postOnCoreThread { + val core = coreContext.core + val state = core.globalState + if (state == GlobalState.Shutdown || state == GlobalState.Off) { + Log.w("$TAG Core is being stopped or already destroyed, abort") + } else { + scope.launch { + parseFriends(core, cursor) + } + } + } } @MainThread override fun onLoaderReset(loader: Loader) { Log.i("$TAG Loader reset") + scope.cancel() } @WorkerThread - private fun parseFriends(cursor: Cursor) { - val core = coreContext.core - - val state = core.globalState - if (state == GlobalState.Shutdown || state == GlobalState.Off) { - Log.w("$TAG Core is being stopped or already destroyed, abort") - return - } - + private fun parseFriends(core: Core, cursor: Cursor) { try { val contactIdColumn = cursor.getColumnIndexOrThrow(ContactsContract.Data.CONTACT_ID) val mimetypeColumn = cursor.getColumnIndexOrThrow(ContactsContract.Data.MIMETYPE) From b40fbcad77a3b598bd38b0b3c2a802c35b63e396 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 25 Mar 2025 14:14:47 +0100 Subject: [PATCH 059/593] Log TelecomManager CallControl failed operations --- .../telecom/TelecomCallControlCallback.kt | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt index 4f71adcaad..d7ac38e0ae 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt @@ -64,7 +64,10 @@ class TelecomCallControlCallback( } else { scope.launch { Log.i("$TAG Setting call active") - callControl.setActive() + val result = callControl.setActive() + if (result is CallControlResult.Error) { + Log.e("$TAG Failed to set call control active: $result") + } } } } else if (state == Call.State.End) { @@ -74,12 +77,18 @@ class TelecomCallControlCallback( } else if (state == Call.State.Pausing) { scope.launch { Log.i("$TAG Pausing call") - callControl.setInactive() + val result = callControl.setInactive() + if (result is CallControlResult.Error) { + Log.e("$TAG Failed to set call control inactive: $result") + } } } else if (state == Call.State.Resuming) { scope.launch { Log.i("$TAG Resuming call") - callControl.setActive() + val result = callControl.setActive() + if (result is CallControlResult.Error) { + Log.e("$TAG Failed to set call control active: $result") + } } } } @@ -279,7 +288,10 @@ class TelecomCallControlCallback( } scope.launch { Log.i("$TAG Answering [${if (isVideo) "video" else "audio"}] call") - callControl.answer(type) + val result = callControl.answer(type) + if (result is CallControlResult.Error) { + Log.e("$TAG Failed to answer call control: $result") + } } if (isVideo && corePreferences.routeAudioToSpeakerWhenVideoIsEnabled) { @@ -306,7 +318,10 @@ class TelecomCallControlCallback( } Log.i("$TAG Disconnecting [${if (direction == Call.Dir.Incoming)"incoming" else "outgoing"}] call with cause [${disconnectCauseToString(disconnectCause)}] because it has ended with reason [$reason]") try { - callControl.disconnect(DisconnectCause(disconnectCause)) + val result = callControl.disconnect(DisconnectCause(disconnectCause)) + if (result is CallControlResult.Error) { + Log.e("$TAG Failed to disconnect call control: $result") + } } catch (ise: IllegalArgumentException) { Log.e("$TAG Couldn't disconnect call control with cause [${disconnectCauseToString(disconnectCause)}]: $ise") } @@ -321,7 +336,10 @@ class TelecomCallControlCallback( val disconnectCause = DisconnectCause.REJECTED Log.w("$TAG Disconnecting call with cause [${disconnectCauseToString(disconnectCause)}] due to error [$message] and reason [$reason]") try { - callControl.disconnect(DisconnectCause(disconnectCause)) + val result = callControl.disconnect(DisconnectCause(disconnectCause)) + if (result is CallControlResult.Error) { + Log.e("$TAG Failed to disconnect call control: $result") + } } catch (ise: IllegalArgumentException) { Log.e("$TAG Couldn't disconnect call control with cause [${disconnectCauseToString(disconnectCause)}]: $ise") } From 9c8c5f309ea7d8132555e01ebf806735342481ba Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 25 Mar 2025 16:27:40 +0100 Subject: [PATCH 060/593] Added hidden setting to allow hiding SIP URIs, show device name instead of SIP full SIP URI when doing trust call from contact details --- .../org/linphone/contacts/ContactsManager.kt | 24 ++++++++++--------- .../java/org/linphone/core/CorePreferences.kt | 4 ++++ .../ui/call/viewmodel/CurrentCallViewModel.kt | 3 +++ .../MessageBottomSheetParticipantModel.kt | 2 +- .../ui/main/chat/model/ParticipantModel.kt | 2 +- .../viewmodel/ConversationInfoViewModel.kt | 3 +++ .../main/contacts/fragment/ContactFragment.kt | 14 +++++------ .../contacts/viewmodel/ContactViewModel.kt | 6 ++--- .../viewmodel/ContactsListViewModel.kt | 2 +- .../history/viewmodel/HistoryViewModel.kt | 3 +++ .../ConversationContactOrSuggestionModel.kt | 7 +++++- .../viewmodel/AccountProfileViewModel.kt | 3 +++ .../res/layout-land/call_active_fragment.xml | 1 + .../res/layout-land/call_ended_fragment.xml | 3 ++- .../layout-land/call_incoming_fragment.xml | 2 +- .../layout-land/call_outgoing_fragment.xml | 3 ++- .../res/layout/account_profile_fragment.xml | 2 ++ .../main/res/layout/call_active_fragment.xml | 2 +- .../main/res/layout/call_ended_fragment.xml | 1 + .../res/layout/call_incoming_fragment.xml | 2 +- .../res/layout/call_outgoing_fragment.xml | 1 + .../main/res/layout/chat_info_fragment.xml | 4 ++-- app/src/main/res/layout/history_fragment.xml | 2 +- 23 files changed, 63 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 291fc6516e..096698d922 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -795,16 +795,18 @@ fun Friend.getListOfSipAddresses(): ArrayList
{ fun Friend.getListOfSipAddressesAndPhoneNumbers(listener: ContactNumberOrAddressClickListener): ArrayList { val addressesAndNumbers = arrayListOf() - for (address in getListOfSipAddresses()) { - val data = ContactNumberOrAddressModel( - this, - address, - address.asStringUriOnly(), - true, // SIP addresses are always enabled - listener, - true - ) - addressesAndNumbers.add(data) + if (!corePreferences.hideSipAddresses) { + for (address in getListOfSipAddresses()) { + val data = ContactNumberOrAddressModel( + this, + address, + address.asStringUriOnly(), + true, // SIP addresses are always enabled + listener, + true + ) + addressesAndNumbers.add(data) + } } if (corePreferences.hidePhoneNumbers) { return addressesAndNumbers @@ -825,7 +827,7 @@ fun Friend.getListOfSipAddressesAndPhoneNumbers(listener: ContactNumberOrAddress if (address != null) { address.clean() // To remove ;user=phone presenceAddress = address - if (addressesAndNumbers.find { it.address?.weakEqual(address) == true } == null) { + if (!corePreferences.hideSipAddresses && addressesAndNumbers.find { it.address?.weakEqual(address) == true } == null) { val data = ContactNumberOrAddressModel( this, address, diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 5e05581f02..9a3718ecdb 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -281,6 +281,10 @@ class CorePreferences val onlyDisplaySipUriUsername: Boolean get() = config.getBool("ui", "only_display_sip_uri_username", false) + @get:WorkerThread + val hideSipAddresses: Boolean + get() = config.getBool("ui", "hide_sip_addresses", false) + @get:WorkerThread val disableChat: Boolean get() = config.getBool("ui", "disable_chat_feature", false) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 7eebbb963e..502a1d28a3 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -145,6 +145,8 @@ class CurrentCallViewModel val qualityIcon = MutableLiveData() + val hideSipAddresses = MutableLiveData() + var terminatedByUser = false val isRemoteRecordingEvent: MutableLiveData>> by lazy { @@ -541,6 +543,7 @@ class CurrentCallViewModel videoUpdateInProgress.value = false coreContext.postOnCoreThread { core -> + hideSipAddresses.postValue(corePreferences.hideSipAddresses) coreContext.contactsManager.addListener(contactsListener) core.addListener(coreListener) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageBottomSheetParticipantModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageBottomSheetParticipantModel.kt index c1c7f69eaa..0480f20aca 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageBottomSheetParticipantModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageBottomSheetParticipantModel.kt @@ -47,7 +47,7 @@ class MessageBottomSheetParticipantModel @UiThread fun clicked() { - if (!isOurOwnReaction && !corePreferences.onlyDisplaySipUriUsername) { + if (!isOurOwnReaction && !corePreferences.onlyDisplaySipUriUsername && !corePreferences.hideSipAddresses) { showSipUri.postValue(showSipUri.value == false) } else { onClick?.invoke() diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ParticipantModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ParticipantModel.kt index c7f171ae61..7367f197cc 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ParticipantModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ParticipantModel.kt @@ -59,7 +59,7 @@ class ParticipantModel @UiThread fun onClicked() { - if (onClicked == null && !corePreferences.onlyDisplaySipUriUsername) { + if (onClicked == null && !corePreferences.onlyDisplaySipUriUsername && !corePreferences.hideSipAddresses) { showSipUri.postValue(showSipUri.value == false) } else { onClicked?.invoke(this) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt index 080120051e..ce12b415bb 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt @@ -56,6 +56,8 @@ class ConversationInfoViewModel val isGroup = MutableLiveData() + val hideSipAddresses = MutableLiveData() + val isEndToEndEncrypted = MutableLiveData() val subject = MutableLiveData() @@ -192,6 +194,7 @@ class ConversationInfoViewModel showPeerSipUri.value = false coreContext.postOnCoreThread { + hideSipAddresses.postValue(corePreferences.hideSipAddresses) coreContext.contactsManager.addListener(contactsListener) } } diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt index 7ccb9d9d1d..164913efb6 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt @@ -221,8 +221,8 @@ class ContactFragment : SlidingPaneChildFragment() { } viewModel.startCallToDeviceToIncreaseTrustEvent.observe(viewLifecycleOwner) { - it.consume { pair -> - callDirectlyOrShowConfirmTrustCallDialog(pair.first, pair.second) + it.consume { triple -> + callDirectlyOrShowConfirmTrustCallDialog(triple.first, triple.second, triple.third) } } @@ -303,18 +303,18 @@ class ContactFragment : SlidingPaneChildFragment() { } private fun showTrustProcessDialog() { - val initials = viewModel.contact.value?.initials?.value ?: "JD" + val initials = viewModel.contact.value?.initials?.value.orEmpty() val picture = viewModel.contact.value?.picturePath?.value.orEmpty() val model = ContactTrustDialogModel(initials, picture) val dialog = DialogUtils.getContactTrustProcessExplanationDialog(requireActivity(), model) dialog.show() } - private fun callDirectlyOrShowConfirmTrustCallDialog(contactName: String, deviceSipUri: String) { + private fun callDirectlyOrShowConfirmTrustCallDialog(contactName: String, deviceName: String, deviceSipUri: String) { coreContext.postOnCoreThread { if (corePreferences.showDialogWhenCallingDeviceUuidDirectly) { coreContext.postOnMainThread { - showConfirmTrustCallDialog(contactName, deviceSipUri) + showConfirmTrustCallDialog(contactName, deviceName, deviceSipUri) } } else { val address = Factory.instance().createAddress(deviceSipUri) @@ -325,11 +325,11 @@ class ContactFragment : SlidingPaneChildFragment() { } } - private fun showConfirmTrustCallDialog(contactName: String, deviceSipUri: String) { + private fun showConfirmTrustCallDialog(contactName: String, deviceName: String, deviceSipUri: String) { val label = AppUtils.getFormattedString( R.string.contact_dialog_increase_trust_level_message, contactName, - deviceSipUri + deviceName ) val model = ConfirmationDialogModel(label) val dialog = DialogUtils.getContactTrustCallConfirmationDialog(requireActivity(), model) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt index a4a3c5c406..98d7da2f63 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt @@ -129,8 +129,8 @@ class ContactViewModel MutableLiveData>() } - val startCallToDeviceToIncreaseTrustEvent: MutableLiveData>> by lazy { - MutableLiveData>>() + val startCallToDeviceToIncreaseTrustEvent: MutableLiveData>> by lazy { + MutableLiveData>>() } val contactRemovedEvent: MutableLiveData> by lazy { @@ -601,7 +601,7 @@ class ContactViewModel ) { if (::friend.isInitialized) { startCallToDeviceToIncreaseTrustEvent.value = - Event(Pair(friend.name.orEmpty(), it.address.asStringUriOnly())) + Event(Triple(friend.name.orEmpty(), it.name, it.address.asStringUriOnly())) } } ) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt index c0396b1a4a..71862217a6 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt @@ -102,7 +102,7 @@ class ContactsListViewModel init { fetchInProgress.value = true showFavourites.value = corePreferences.showFavoriteContacts - showFilter.value = !corePreferences.hidePhoneNumbers + showFilter.value = !corePreferences.hidePhoneNumbers && !corePreferences.hideSipAddresses coreContext.postOnCoreThread { core -> updateDomainFilter() diff --git a/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt b/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt index 6f96dda670..e89c6990cf 100644 --- a/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt @@ -63,6 +63,8 @@ class HistoryViewModel val isChatRoomAvailable = MutableLiveData() + val hideSipAddresses = MutableLiveData() + val callLogFoundEvent = MutableLiveData>() val chatRoomCreationErrorEvent: MutableLiveData> by lazy { @@ -135,6 +137,7 @@ class HistoryViewModel core.addListener(coreListener) chatDisabled.postValue(corePreferences.disableChat) videoCallDisabled.postValue(!core.isVideoEnabled) + hideSipAddresses.postValue(corePreferences.hideSipAddresses) } } diff --git a/app/src/main/java/org/linphone/ui/main/model/ConversationContactOrSuggestionModel.kt b/app/src/main/java/org/linphone/ui/main/model/ConversationContactOrSuggestionModel.kt index 1c53b546e1..8ad0e6640c 100644 --- a/app/src/main/java/org/linphone/ui/main/model/ConversationContactOrSuggestionModel.kt +++ b/app/src/main/java/org/linphone/ui/main/model/ConversationContactOrSuggestionModel.kt @@ -22,6 +22,7 @@ package org.linphone.ui.main.model import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData +import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.core.Address import org.linphone.core.Friend import org.linphone.ui.main.contacts.model.ContactAvatarModel @@ -48,7 +49,11 @@ class ConversationContactOrSuggestionModel address.username.orEmpty() } - val sipUri = address.asStringUriOnly() + val sipUri = if (!corePreferences.hideSipAddresses) { + address.asStringUriOnly() + } else { + address.username + } val initials = AppUtils.getInitials(conversationSubject ?: name) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt index 617e827471..b520f0cc80 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt @@ -86,6 +86,8 @@ class AccountProfileViewModel val hideAccountSettings = MutableLiveData() + val hideSipAddresses = MutableLiveData() + val deviceId = MutableLiveData() val showDeviceId = MutableLiveData() @@ -179,6 +181,7 @@ class AccountProfileViewModel coreContext.postOnCoreThread { hideAccountSettings.postValue(corePreferences.hideAccountSettings) + hideSipAddresses.postValue(corePreferences.hideSipAddresses) dialPlansLabelList.add("") // To allow removing selected dial plan val dialPlans = Factory.instance().dialPlans.toList() diff --git a/app/src/main/res/layout-land/call_active_fragment.xml b/app/src/main/res/layout-land/call_active_fragment.xml index 43d947f0c4..aa8d96528e 100644 --- a/app/src/main/res/layout-land/call_active_fragment.xml +++ b/app/src/main/res/layout-land/call_active_fragment.xml @@ -100,6 +100,7 @@ android:id="@+id/address" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:visibility="@{viewModel.hideSipAddresses ? View.GONE : View.VISIBLE}" android:text="@{viewModel.displayedAddress, default=`sip:johndoe@sip.linphone.org`}" android:textColor="@color/bc_white" android:textSize="14sp" /> diff --git a/app/src/main/res/layout-land/call_ended_fragment.xml b/app/src/main/res/layout-land/call_ended_fragment.xml index 2a8e0554aa..209f04667a 100644 --- a/app/src/main/res/layout-land/call_ended_fragment.xml +++ b/app/src/main/res/layout-land/call_ended_fragment.xml @@ -78,7 +78,8 @@ android:layout_height="wrap_content" android:text="@{viewModel.displayedAddress, default=`sip:johndoe@sip.linphone.org`}" android:textColor="@color/bc_white" - android:textSize="14sp" /> + android:textSize="14sp" + android:visibility="@{viewModel.hideSipAddresses ? View.GONE : View.VISIBLE}" /> diff --git a/app/src/main/res/layout-land/call_incoming_fragment.xml b/app/src/main/res/layout-land/call_incoming_fragment.xml index 26859bbc70..414a8b0637 100644 --- a/app/src/main/res/layout-land/call_incoming_fragment.xml +++ b/app/src/main/res/layout-land/call_incoming_fragment.xml @@ -59,7 +59,7 @@ android:text="@{viewModel.displayedAddress, default=`sip:johndoe@sip.linphone.org`}" android:textColor="@color/bc_white" android:textSize="14sp" - android:visibility="@{viewModel.conferenceModel.isCurrentCallInConference ? View.GONE : View.VISIBLE}" /> + android:visibility="@{viewModel.hideSipAddresses || viewModel.conferenceModel.isCurrentCallInConference ? View.GONE : View.VISIBLE}" /> diff --git a/app/src/main/res/layout-land/call_outgoing_fragment.xml b/app/src/main/res/layout-land/call_outgoing_fragment.xml index 2f9c0292fc..63960a174f 100644 --- a/app/src/main/res/layout-land/call_outgoing_fragment.xml +++ b/app/src/main/res/layout-land/call_outgoing_fragment.xml @@ -80,7 +80,8 @@ android:layout_height="wrap_content" android:text="@{viewModel.displayedAddress, default=`sip:johndoe@sip.linphone.org`}" android:textColor="@color/bc_white" - android:textSize="14sp" /> + android:textSize="14sp" + android:visibility="@{viewModel.hideSipAddresses ? View.GONE : View.VISIBLE}" /> diff --git a/app/src/main/res/layout/account_profile_fragment.xml b/app/src/main/res/layout/account_profile_fragment.xml index 50e6706ca9..6d23845d30 100644 --- a/app/src/main/res/layout/account_profile_fragment.xml +++ b/app/src/main/res/layout/account_profile_fragment.xml @@ -213,6 +213,7 @@ android:paddingTop="8dp" android:paddingBottom="8dp" android:text="@string/sip_address" + android:visibility="@{!viewModel.hideSipAddresses || viewModel.showDeviceId ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintStart_toStartOf="@id/details_background" app:layout_constraintTop_toTopOf="@id/details_background"/> @@ -233,6 +234,7 @@ android:drawableEnd="@drawable/copy" android:drawablePadding="5dp" app:drawableTint="?attr/color_main2_600" + android:visibility="@{!viewModel.hideSipAddresses || viewModel.showDeviceId ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintStart_toEndOf="@id/sip_address_label" app:layout_constraintEnd_toEndOf="@id/details_background" app:layout_constraintTop_toTopOf="@id/sip_address_label" diff --git a/app/src/main/res/layout/call_active_fragment.xml b/app/src/main/res/layout/call_active_fragment.xml index 1d88bb5383..0e638cfe17 100644 --- a/app/src/main/res/layout/call_active_fragment.xml +++ b/app/src/main/res/layout/call_active_fragment.xml @@ -98,7 +98,7 @@ android:text="@{viewModel.displayedAddress, default=`sip:johndoe@sip.linphone.org`}" android:textColor="@color/bc_white" android:textSize="14sp" - android:visibility="@{viewModel.pipMode || viewModel.halfOpenedFolded ? View.GONE : View.VISIBLE}" + android:visibility="@{viewModel.hideSipAddresses || viewModel.pipMode || viewModel.halfOpenedFolded ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toBottomOf="@id/display_name" app:layout_constraintBottom_toBottomOf="@id/hinge_bottom" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/call_ended_fragment.xml b/app/src/main/res/layout/call_ended_fragment.xml index dfc8431d64..1659786bc2 100644 --- a/app/src/main/res/layout/call_ended_fragment.xml +++ b/app/src/main/res/layout/call_ended_fragment.xml @@ -75,6 +75,7 @@ android:text="@{viewModel.displayedAddress, default=`sip:johndoe@sip.linphone.org`}" android:textColor="@color/bc_white" android:textSize="14sp" + android:visibility="@{viewModel.hideSipAddresses ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toBottomOf="@id/name" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/call_incoming_fragment.xml b/app/src/main/res/layout/call_incoming_fragment.xml index c8b55177ff..96e3c48956 100644 --- a/app/src/main/res/layout/call_incoming_fragment.xml +++ b/app/src/main/res/layout/call_incoming_fragment.xml @@ -62,7 +62,7 @@ android:text="@{viewModel.displayedAddress, default=`sip:johndoe@sip.linphone.org`}" android:textColor="@color/bc_white" android:textSize="14sp" - android:visibility="@{viewModel.conferenceModel.isCurrentCallInConference ? View.GONE : View.VISIBLE}" + android:visibility="@{viewModel.hideSipAddresses || viewModel.conferenceModel.isCurrentCallInConference ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toBottomOf="@id/name" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/call_outgoing_fragment.xml b/app/src/main/res/layout/call_outgoing_fragment.xml index fe69ccf882..97026af949 100644 --- a/app/src/main/res/layout/call_outgoing_fragment.xml +++ b/app/src/main/res/layout/call_outgoing_fragment.xml @@ -100,6 +100,7 @@ android:text="@{viewModel.displayedAddress, default=`sip:johndoe@sip.linphone.org`}" android:textColor="@color/bc_white" android:textSize="14sp" + android:visibility="@{viewModel.hideSipAddresses ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toBottomOf="@id/name" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/chat_info_fragment.xml b/app/src/main/res/layout/chat_info_fragment.xml index 68a0ac2549..f63d9dfece 100644 --- a/app/src/main/res/layout/chat_info_fragment.xml +++ b/app/src/main/res/layout/chat_info_fragment.xml @@ -155,7 +155,7 @@ android:textSize="14sp" android:drawableEnd="@drawable/copy" android:drawablePadding="5dp" - android:visibility="@{viewModel.isGroup ? View.GONE : View.VISIBLE, default=gone}" + android:visibility="@{viewModel.isGroup || viewModel.hideSipAddresses ? View.GONE : View.VISIBLE, default=gone}" app:drawableTint="?attr/color_main2_600" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -176,7 +176,7 @@ android:textSize="14sp" android:drawableEnd="@drawable/copy" android:drawablePadding="5dp" - android:visibility="@{viewModel.showPeerSipUri ? View.VISIBLE : View.GONE, default=gone}" + android:visibility="@{viewModel.showPeerSipUri && !viewModel.hideSipAddresses ? View.VISIBLE : View.GONE, default=gone}" app:drawableTint="?attr/color_main2_600" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/history_fragment.xml b/app/src/main/res/layout/history_fragment.xml index 4f9d0cc50f..cbc638fa05 100644 --- a/app/src/main/res/layout/history_fragment.xml +++ b/app/src/main/res/layout/history_fragment.xml @@ -129,7 +129,7 @@ android:textSize="14sp" android:maxLines="1" android:ellipsize="end" - android:visibility="@{viewModel.isConferenceCallLog ? View.GONE : View.VISIBLE}" + android:visibility="@{viewModel.isConferenceCallLog || viewModel.hideSipAddresses ? View.GONE : View.VISIBLE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/name" /> From fbc19c7053f23aaa03eef12fbabe161592a0e0a2 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 25 Mar 2025 16:45:56 +0100 Subject: [PATCH 061/593] Fixes regarding contacts list filter when switching account --- .../viewmodel/ContactsListViewModel.kt | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt index 71862217a6..4bdcf109e5 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt @@ -38,7 +38,6 @@ import org.linphone.core.MagicSearchListenerStub import org.linphone.core.SearchResult import org.linphone.core.tools.Log import org.linphone.ui.main.contacts.model.ContactAvatarModel -import org.linphone.ui.main.model.isEndToEndEncryptionMandatory import org.linphone.ui.main.viewmodel.AbstractMainViewModel import org.linphone.utils.Event import org.linphone.utils.FileUtils @@ -143,9 +142,11 @@ class ContactsListViewModel fun applyCurrentDefaultAccountFilter() { coreContext.postOnCoreThread { updateDomainFilter() - } - applyFilter(currentFilter) + coreContext.postOnMainThread { + applyFilter(currentFilter) + } + } } @UiThread @@ -181,17 +182,22 @@ class ContactsListViewModel @WorkerThread private fun updateDomainFilter() { val defaultAccount = coreContext.core.defaultAccount - val defaultDomain = defaultAccount?.params?.domain == corePreferences.defaultDomain - isDefaultAccountLinphone.postValue(defaultDomain) + val defaultDomain = corePreferences.defaultDomain + val isAccountOnDefaultDomain = defaultAccount?.params?.domain == defaultDomain + isDefaultAccountLinphone.postValue(isAccountOnDefaultDomain) - Log.i("$TAG Currently selected filter is [${corePreferences.contactsFilter}]") domainFilter = corePreferences.contactsFilter - if (isEndToEndEncryptionMandatory() && (domainFilter.isEmpty() || domainFilter == "*")) { - domainFilter = corePreferences.defaultDomain + Log.i("$TAG Currently selected filter is [$domainFilter]") + if (!isAccountOnDefaultDomain && domainFilter == defaultDomain) { + domainFilter = "*" corePreferences.contactsFilter = domainFilter Log.i( - "$TAG Filter updated to [${corePreferences.contactsFilter}] to match mandatory IM encryption" + "$TAG New default account isn't on default domain, changing filter to all SIP contacts instead" ) + } else if (isAccountOnDefaultDomain && domainFilter != "") { + domainFilter = defaultDomain + corePreferences.contactsFilter = domainFilter + Log.i("$TAG New default account is on default domain, using that as filter instead") } } From a503ef06ee43abba42b8a64c42e8b8f8da75e1f5 Mon Sep 17 00:00:00 2001 From: Peio Rigaux Date: Fri, 21 Mar 2025 17:43:04 +0100 Subject: [PATCH 062/593] Now use docker to deploy. Allows multiple deploy at the same time --- .gitlab-ci-files/job-upload.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci-files/job-upload.yml b/.gitlab-ci-files/job-upload.yml index 7ba81e8b4c..ddb1be1402 100644 --- a/.gitlab-ci-files/job-upload.yml +++ b/.gitlab-ci-files/job-upload.yml @@ -1,12 +1,20 @@ job-android-upload: stage: deploy - tags: [ "deploy" ] + tags: [ "docker-deploy" ] only: - schedules dependencies: - job-android + before_script: + - if ! [ -z ${SCP_PRIVATE_KEY+x} ] && ! [ -z ${DEPLOY_SERVER_HOST_KEYS+x} ]; then eval $(ssh-agent -s); fi + - if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then echo "$SCP_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null; fi + - if ! [ -z ${DEPLOY_SERVER_HOST_KEYS+x} ]; then mkdir -p ~/.ssh && chmod 700 ~/.ssh; fi + - if ! [ -z ${DEPLOY_SERVER_HOST_KEYS+x} ]; then echo "$DEPLOY_SERVER_HOST_KEYS" >> ~/.ssh/known_hosts; fi + script: - - cd app/build/outputs/apk/ && rsync ./release/*.apk $DEPLOY_SERVER:$ANDROID_DEPLOY_DIRECTORY \ No newline at end of file + # Launches rsync in partial mode, which means that we are using a temp_dir in case of a transfer issue + # Upon a job relaunch, the files in temp_dir would then be re-used, and deleted if the transfer succeeds + - cd app/build/outputs/apk/ && rsync --partial --partial-dir=$CI_PIPELINE_ID_$CI_JOB_NAME ./release/*.apk $DEPLOY_SERVER:$ANDROID_DEPLOY_DIRECTORY \ No newline at end of file From a0108776dd88af62314b858ecad80f0cd85c7849 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 25 Mar 2025 16:54:19 +0100 Subject: [PATCH 063/593] Keep newly created account disabled until SMS code validation is done --- .../viewmodel/AccountCreationViewModel.kt | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt index db9ba778ef..2681eda1c2 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt @@ -142,14 +142,7 @@ class AccountCreationViewModel goToSmsCodeConfirmationViewEvent.postValue(Event(true)) } AccountManagerServicesRequest.Type.LinkPhoneNumberUsingCode -> { - val account = accountCreated - if (account != null) { - Log.i( - "$TAG Account [${account.params.identityAddress?.asStringUriOnly()}] has been created & activated, setting it as default" - ) - coreContext.core.defaultAccount = account - } - accountCreatedEvent.postValue(Event(true)) + enableAccountAndSetItAsDefault() } else -> { } } @@ -496,6 +489,9 @@ class AccountCreationViewModel ) accountParams.internationalPrefix = dialPlan.internationalCallPrefix accountParams.internationalPrefixIsoCountryCode = dialPlan.isoCountryCode + + // Do not enable account just yet, wait for it to be activated using SMS code + accountParams.isRegisterEnabled = false } val account = core.createAccount(accountParams) core.addAccount(account) @@ -507,6 +503,21 @@ class AccountCreationViewModel lockUsernameAndPassword.postValue(true) } + @WorkerThread + private fun enableAccountAndSetItAsDefault() { + val account = accountCreated ?: return + Log.i( + "$TAG Account [${account.params.identityAddress?.asStringUriOnly()}] has been created & activated, enable it & setting it as default" + ) + + val newParams = account.params.clone() + newParams.isRegisterEnabled = true + account.params = newParams + + coreContext.core.defaultAccount = account + accountCreatedEvent.postValue(Event(true)) + } + @WorkerThread private fun requestFlexiApiToken() { if (!coreContext.core.isPushNotificationAvailable) { From c528f0cdb8059e0a41ebf13ad038700837012574 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 25 Mar 2025 17:14:06 +0100 Subject: [PATCH 064/593] Bumped dependencies --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 130853c9bf..b7cd88a115 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] -agp = "8.9.0" +agp = "8.9.1" kotlin = "2.0.21" gmsGoogleServices = "4.4.2" firebaseCrashlytics = "3.0.3" -firebaseBomVersion = "33.10.0" +firebaseBomVersion = "33.11.0" ktlint = "12.1.2" annotations = "1.9.1" @@ -19,7 +19,7 @@ slidingpanelayout = "1.2.0" window = "1.3.0" gridlayout = "1.0.0" securityCryptoKtx = "1.1.0-alpha06" -navigation = "2.8.8" +navigation = "2.8.9" emoji2 = "1.5.0" car = "1.7.0-rc01" flexbox = "3.0.0" From 1f45ba8bd0755a497b4e13e520916538133debcb Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 26 Mar 2025 09:19:37 +0100 Subject: [PATCH 065/593] Added back ring during early media setting --- .../settings/viewmodel/SettingsViewModel.kt | 12 ++++++++ .../res/layout/settings_advanced_calls.xml | 30 ++++++++++++++++++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 2ed4bd630c..4324b19e29 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -211,6 +211,7 @@ class SettingsViewModel val mediaEncryptionMandatory = MutableLiveData() val createEndToEndEncryptedConferences = MutableLiveData() val acceptEarlyMedia = MutableLiveData() + val ringDuringEarlyMedia = MutableLiveData() val allowOutgoingEarlyMedia = MutableLiveData() val autoAnswerIncomingCalls = MutableLiveData() val autoAnswerIncomingCallsDelay = MutableLiveData() @@ -338,6 +339,7 @@ class SettingsViewModel createEndToEndEncryptedConferences.postValue(corePreferences.createEndToEndEncryptedMeetingsAndGroupCalls) acceptEarlyMedia.postValue(corePreferences.acceptEarlyMedia) + ringDuringEarlyMedia.postValue(core.ringDuringIncomingEarlyMedia) allowOutgoingEarlyMedia.postValue(corePreferences.allowOutgoingEarlyMedia) autoAnswerIncomingCalls.postValue(corePreferences.autoAnswerEnabled) autoAnswerIncomingCallsDelay.postValue(corePreferences.autoAnswerDelay) @@ -826,6 +828,16 @@ class SettingsViewModel } } + @UiThread + fun toggleRingDuringEarlyMedia() { + val newValue = ringDuringEarlyMedia.value == false + + coreContext.postOnCoreThread { core -> + core.ringDuringIncomingEarlyMedia = newValue + ringDuringEarlyMedia.postValue(newValue) + } + } + @UiThread fun toggleAllowOutgoingEarlyMedia() { val newValue = allowOutgoingEarlyMedia.value == false diff --git a/app/src/main/res/layout/settings_advanced_calls.xml b/app/src/main/res/layout/settings_advanced_calls.xml index d65c942314..cce05fca2e 100644 --- a/app/src/main/res/layout/settings_advanced_calls.xml +++ b/app/src/main/res/layout/settings_advanced_calls.xml @@ -223,6 +223,34 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/e2e_encrypted_conferences_switch" /> + + + + + app:layout_constraintTop_toBottomOf="@id/ring_during_early_media_switch" /> Rendre le chiffrement du média obligatoire Créer en mode chiffré de bout en bout les réunions et les appels de groupe Accepter l\'early media + Sonner pendant un appel entrant avec early-media Autoriser l\'early media pour les appels sortants Décrocher automatiquement les appels entrants Délai avant le décrochage automatique diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 492ac6b3b3..d8016d6a41 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -313,6 +313,7 @@ Media encryption mandatory Create end-to-end encrypted meetings & group calls Accept early media + Ring during incoming early media call Allow outgoing early media Auto answer incoming calls Delay before auto answering call From 90bf20e50edad96dfdc793cbff5c4f6d870ec082 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 26 Mar 2025 09:48:03 +0100 Subject: [PATCH 066/593] Prevent meeting icons from being briefly visible when selected account has no videoconference factory URI set in it's params --- app/src/main/java/org/linphone/core/CoreContext.kt | 9 +++++++++ .../ui/main/viewmodel/AbstractMainViewModel.kt | 14 ++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 1cd2500b88..e56136b06e 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -82,6 +82,8 @@ class CoreContext private val mainThread = Handler(Looper.getMainLooper()) + var defaultAccountHasVideoConferenceFactoryUri: Boolean = false + var bearerAuthInfoPendingPasswordUpdate: AuthInfo? = null var digestAuthInfoPendingPasswordUpdate: AuthInfo? = null @@ -175,6 +177,11 @@ class CoreContext private var previousCallState = Call.State.Idle private val coreListener = object : CoreListenerStub() { + @WorkerThread + override fun onDefaultAccountChanged(core: Core, account: Account?) { + defaultAccountHasVideoConferenceFactoryUri = account?.params?.audioVideoConferenceFactoryAddress != null + } + @WorkerThread override fun onMessagesReceived( core: Core, @@ -512,6 +519,8 @@ class CoreContext core.isAutoIterateEnabled = true core.addListener(coreListener) + defaultAccountHasVideoConferenceFactoryUri = core.defaultAccount?.params?.audioVideoConferenceFactoryAddress != null + coreThread.postDelayed({ startCore() }, 50) Looper.loop() diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractMainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractMainViewModel.kt index 1b3b82b5cf..cfd562bb1f 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractMainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractMainViewModel.kt @@ -148,6 +148,10 @@ open class AbstractMainViewModel @WorkerThread override fun onDefaultAccountChanged(core: Core, defaultAccount: Account?) { + updateAvailableMenus() + computeUnreadMessagesCount() + updateMissedCallsCount() + account.value?.destroy() if (defaultAccount == null) { @@ -162,15 +166,14 @@ open class AbstractMainViewModel account.postValue(AccountModel(defaultAccount)) } - computeUnreadMessagesCount() - updateMissedCallsCount() - updateAvailableMenus() - defaultAccountChangedEvent.postValue(Event(true)) } } init { + // Pre-compute this value to prevent the menu being briefly visible + hideMeetings.value = !coreContext.defaultAccountHasVideoConferenceFactoryUri + coreContext.postOnCoreThread { core -> core.addListener(coreListener) configure() @@ -301,8 +304,7 @@ open class AbstractMainViewModel val conferencingAvailable = LinphoneUtils.isRemoteConferencingAvailable( coreContext.core ) - val hideGroupCall = - coreContext.core.accountList.isEmpty() || corePreferences.disableMeetings || !conferencingAvailable + val hideGroupCall = corePreferences.disableMeetings || !conferencingAvailable hideMeetings.postValue(hideGroupCall) } From 2621eb306e5f96676295264234889aac02f57910 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 26 Mar 2025 16:21:55 +0100 Subject: [PATCH 067/593] Added content message to keep app alive foreground service notification --- .../main/java/org/linphone/notifications/NotificationsManager.kt | 1 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 3 files changed, 3 insertions(+) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 4cd86eec8d..77a8bf5077 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -1558,6 +1558,7 @@ class NotificationsManager val builder = NotificationCompat.Builder(context, channelId) .setSmallIcon(R.drawable.linphone_notification) + .setContentText(AppUtils.getString(R.string.notification_keep_app_alive_message)) .setAutoCancel(false) .setOngoing(true) .setCategory(NotificationCompat.CATEGORY_SERVICE) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index bdad6c7fa3..188f94e567 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -68,6 +68,7 @@ %s fichier en cours de réception %s fichiers en cours de réception + Cliquez pour ouvrir Bienvenue diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d8016d6a41..dcc2eb60a7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -108,6 +108,7 @@ %s files being downloaded %s, %s + Click to open Welcome From f0ad67fb2949c3b1eb0d5cf0a7548acef91d507f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 27 Mar 2025 09:10:22 +0100 Subject: [PATCH 068/593] Fixed disabled handle color while outgoing call is ringing --- app/src/main/res/layout/call_outgoing_actions.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/call_outgoing_actions.xml b/app/src/main/res/layout/call_outgoing_actions.xml index 44ac50decb..28063ea491 100644 --- a/app/src/main/res/layout/call_outgoing_actions.xml +++ b/app/src/main/res/layout/call_outgoing_actions.xml @@ -23,7 +23,7 @@ android:src="@drawable/animated_caret_to_handle" android:enabled="false" android:contentDescription="@string/content_description_toggle_bottom_sheet" - app:tint="@color/in_call_button_tint_color" + app:tint="@color/gray_400" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> From 7bead679ad46fbcabab9bc268177fbab984ff00f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 26 Mar 2025 14:51:03 +0100 Subject: [PATCH 069/593] Delete all data related to account being removed --- .../viewmodel/AccountProfileViewModel.kt | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt index b520f0cc80..6012728e8d 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt @@ -268,9 +268,20 @@ class AccountProfileViewModel fun deleteAccount() { coreContext.postOnCoreThread { core -> if (::account.isInitialized) { + Log.i("$TAG Removing call logs, conversations & meetings related to account being removed") + account.clearCallLogs() + + for (conversation in account.chatRooms) { + core.deleteChatRoom(conversation) + } + for (meeting in account.conferenceInformationList) { + core.deleteConferenceInformation(meeting) + } + + val identity = account.params.identityAddress?.asStringUriOnly() val authInfo = account.findAuthInfo() if (authInfo != null) { - Log.i("$TAG Found auth info for account, removing it") + Log.i("$TAG Found auth info for account [$identity], removing it") if (authInfo.password.isNullOrEmpty() && authInfo.ha1.isNullOrEmpty() && authInfo.accessToken != null) { Log.i("$TAG Auth info was using bearer token instead of password") val ssoCache = File(corePreferences.ssoCacheFile) @@ -283,11 +294,11 @@ class AccountProfileViewModel } core.removeAuthInfo(authInfo) } else { - Log.w("$TAG Failed to find matching auth info for account") + Log.w("$TAG Failed to find matching auth info for account [$identity]") } core.removeAccount(account) - Log.i("$TAG Account has been removed") + Log.i("$TAG Account [$identity] has been removed") accountRemovedEvent.postValue(Event(true)) } } From 18e15b60a4aae95bd5e8e903d2bd79543fe253e9 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 27 Mar 2025 10:13:59 +0100 Subject: [PATCH 070/593] Fixed toggle setting color when disabled & off, disable early media ringing toggle if early media ringing is disabled --- app/src/main/res/color/switch_track_color.xml | 3 ++- app/src/main/res/layout/settings_advanced_calls.xml | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/color/switch_track_color.xml b/app/src/main/res/color/switch_track_color.xml index 1a475be6d4..907249168c 100644 --- a/app/src/main/res/color/switch_track_color.xml +++ b/app/src/main/res/color/switch_track_color.xml @@ -1,6 +1,7 @@ - + + diff --git a/app/src/main/res/layout/settings_advanced_calls.xml b/app/src/main/res/layout/settings_advanced_calls.xml index cce05fca2e..bac6ae83ea 100644 --- a/app/src/main/res/layout/settings_advanced_calls.xml +++ b/app/src/main/res/layout/settings_advanced_calls.xml @@ -234,6 +234,7 @@ android:text="@string/settings_advanced_ring_during_early_media_title" android:maxLines="2" android:ellipsize="end" + android:enabled="@{viewModel.acceptEarlyMedia}" app:layout_constraintTop_toTopOf="@id/ring_during_early_media_switch" app:layout_constraintBottom_toBottomOf="@id/ring_during_early_media_switch" app:layout_constraintStart_toStartOf="parent" @@ -247,6 +248,7 @@ android:layout_height="wrap_content" android:layout_marginTop="20dp" android:layout_marginEnd="16dp" + android:enabled="@{viewModel.acceptEarlyMedia}" android:checked="@{viewModel.ringDuringEarlyMedia}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/accept_early_media_switch" /> From 689665c47553367685b87045d63dec59c0933a93 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 27 Mar 2025 11:32:11 +0100 Subject: [PATCH 071/593] Show floating action button to open numpad in outgoing early media call, prevent display name & SIP address being displayed twice if early media is audio only --- .../ui/call/fragment/OutgoingCallFragment.kt | 12 + .../in_call_button_background_tint_color.xml | 6 + .../layout-land/call_outgoing_fragment.xml | 262 +++++++++-------- .../res/layout/call_incoming_fragment.xml | 4 +- .../res/layout/call_outgoing_fragment.xml | 264 ++++++++++-------- 5 files changed, 313 insertions(+), 235 deletions(-) create mode 100644 app/src/main/res/color/in_call_button_background_tint_color.xml diff --git a/app/src/main/java/org/linphone/ui/call/fragment/OutgoingCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/OutgoingCallFragment.kt index e4b0bdbd35..ffbb9714a4 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/OutgoingCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/OutgoingCallFragment.kt @@ -26,6 +26,7 @@ import android.view.ViewGroup import androidx.annotation.UiThread import androidx.core.view.doOnLayout import androidx.lifecycle.ViewModelProvider +import com.google.android.material.bottomsheet.BottomSheetBehavior import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.core.tools.Log import org.linphone.databinding.CallOutgoingFragmentBinding @@ -60,6 +61,7 @@ class OutgoingCallFragment : GenericCallFragment() { binding.lifecycleOwner = viewLifecycleOwner binding.viewModel = callViewModel + binding.numpadModel = callViewModel.numpadModel callViewModel.isOutgoingEarlyMedia.observe(viewLifecycleOwner) { earlyMedia -> if (earlyMedia) { @@ -69,6 +71,16 @@ class OutgoingCallFragment : GenericCallFragment() { } } } + + val numpadBottomSheetBehavior = BottomSheetBehavior.from(binding.callNumpad.root) + numpadBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN + numpadBottomSheetBehavior.skipCollapsed = true + + callViewModel.showNumpadBottomSheetEvent.observe(viewLifecycleOwner) { + it.consume { + numpadBottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } + } } override fun onResume() { diff --git a/app/src/main/res/color/in_call_button_background_tint_color.xml b/app/src/main/res/color/in_call_button_background_tint_color.xml new file mode 100644 index 0000000000..67e4952cbd --- /dev/null +++ b/app/src/main/res/color/in_call_button_background_tint_color.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/layout-land/call_outgoing_fragment.xml b/app/src/main/res/layout-land/call_outgoing_fragment.xml index 63960a174f..c936b50270 100644 --- a/app/src/main/res/layout-land/call_outgoing_fragment.xml +++ b/app/src/main/res/layout-land/call_outgoing_fragment.xml @@ -8,134 +8,164 @@ + - - - - - + android:layout_height="match_parent"> - - - + + + + android:layout_marginStart="10dp" + android:layout_marginTop="10dp" + android:layout_marginBottom="10dp" + android:text="@string/call_outgoing" + app:layout_constraintStart_toEndOf="@id/call_direction_icon" + app:layout_constraintTop_toTopOf="parent"/> - + + + + + + + + + + + + + + - - - - - - - - + android:layout_marginEnd="10dp" + android:layout_marginBottom="10dp" + app:alignTopRight="true" + app:displayMode="black_bars" + roundCornersRadius="@dimen/call_round_corners_texture_view_radius" + app:layout_constraintBottom_toTopOf="@id/bottom_bar" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHeight_max="@dimen/call_video_preview_max_size" + app:layout_constraintWidth_max="@dimen/call_video_preview_max_size" /> + + + + + + - - + android:id="@+id/call_numpad" + layout="@layout/call_numpad_bottom_sheet" + bind:model="@{numpadModel}"/> + + \ No newline at end of file diff --git a/app/src/main/res/layout/call_incoming_fragment.xml b/app/src/main/res/layout/call_incoming_fragment.xml index 96e3c48956..7d0c19e68e 100644 --- a/app/src/main/res/layout/call_incoming_fragment.xml +++ b/app/src/main/res/layout/call_incoming_fragment.xml @@ -110,7 +110,7 @@ android:text="@{viewModel.displayedName, default=`John Doe`}" android:textColor="@color/bc_white" android:textSize="22sp" - android:visibility="@{viewModel.isIncomingEarlyMedia ? View.VISIBLE : View.GONE, default=gone}" + android:visibility="@{viewModel.isIncomingEarlyMedia && viewModel.isVideoEnabled ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintBottom_toTopOf="@id/early_media_address" app:layout_constraintStart_toStartOf="parent" /> @@ -124,7 +124,7 @@ android:text="@{viewModel.displayedAddress, default=`sip:johndoe@sip.linphone.org`}" android:textColor="@color/bc_white" android:textSize="14sp" - android:visibility="@{viewModel.isIncomingEarlyMedia ? View.VISIBLE : View.GONE, default=gone}" + android:visibility="@{viewModel.isIncomingEarlyMedia && viewModel.isVideoEnabled ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintBottom_toTopOf="@id/bottom_bar" app:layout_constraintStart_toStartOf="parent" /> diff --git a/app/src/main/res/layout/call_outgoing_fragment.xml b/app/src/main/res/layout/call_outgoing_fragment.xml index 97026af949..8ff439d8cf 100644 --- a/app/src/main/res/layout/call_outgoing_fragment.xml +++ b/app/src/main/res/layout/call_outgoing_fragment.xml @@ -8,128 +8,158 @@ + - - - - - - - - - + android:layout_height="match_parent"> - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - + android:id="@+id/call_numpad" + layout="@layout/call_numpad_bottom_sheet" + bind:model="@{numpadModel}"/> + + \ No newline at end of file From 317a7c44170777cb7a294ccc0078241f48bb16fc Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 27 Mar 2025 15:28:57 +0100 Subject: [PATCH 072/593] Trying to prevent bottom bar from disappearing sometimes --- app/src/main/res/layout/chat_list_fragment.xml | 5 ++--- app/src/main/res/layout/contacts_list_fragment.xml | 7 +++---- app/src/main/res/layout/history_list_fragment.xml | 5 ++--- app/src/main/res/layout/meetings_list_fragment.xml | 5 ++--- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/app/src/main/res/layout/chat_list_fragment.xml b/app/src/main/res/layout/chat_list_fragment.xml index ca9901314b..b59c17bbc8 100644 --- a/app/src/main/res/layout/chat_list_fragment.xml +++ b/app/src/main/res/layout/chat_list_fragment.xml @@ -58,10 +58,9 @@ android:id="@+id/conversations_list" android:background="@drawable/shape_squircle_white_r20_top_background" android:layout_width="match_parent" - android:layout_height="0dp" + android:layout_height="match_parent" android:layout_marginTop="@dimen/top_bar_height" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar" /> + android:layout_marginBottom="@dimen/portrait_nav_bar_height" /> + app:layout_constraintStart_toStartOf="parent"> + android:layout_marginBottom="@dimen/portrait_nav_bar_height" /> + android:layout_marginBottom="@dimen/portrait_nav_bar_height" /> Date: Thu, 27 Mar 2025 13:22:04 +0100 Subject: [PATCH 073/593] Fixed LDAP/remote CardDAV results not always displayed when making a search in contacts list --- .../ui/main/contacts/viewmodel/ContactViewModel.kt | 4 +++- .../ui/main/contacts/viewmodel/ContactsListViewModel.kt | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt index 98d7da2f63..2667c2d01b 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt @@ -309,7 +309,9 @@ class ContactViewModel @WorkerThread fun refreshContactInfo() { isFavourite.postValue(friend.starred) - isStored.postValue(friend.inList()) + // Do not show edit contact button for contacts not stored in a FriendList or + // if they are in a temporary one (for example if they are from a remote directory such as LDAP or CardDAV) + isStored.postValue(!coreContext.contactsManager.isContactTemporary(friend)) contact.value?.destroy() contact.postValue(ContactAvatarModel(friend)) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt index 4bdcf109e5..e4064861b6 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt @@ -296,7 +296,13 @@ class ContactsListViewModel for (result in results) { val friend = result.friend if (friend != null) { - if (coreContext.contactsManager.isContactTemporary(friend, allowNullFriendList = true)) continue + val isFromRemoteDirectory = result.hasSourceFlag(MagicSearch.Source.LdapServers) || result.hasSourceFlag(MagicSearch.Source.RemoteCardDAV) + // Only display friends from temporary friend lists if their source flag show they + // were fetched from a remote contact directory (and not the local friend list) + if (!isFromRemoteDirectory && coreContext.contactsManager.isContactTemporary(friend, allowNullFriendList = true)) { + Log.i("$TAG Do not show friend [${friend.name}] which is in a temporary friend list") + continue + } if (friend.refKey.orEmpty().isEmpty()) { if (friend.vcard != null) { From 1cccf7d26bd94cb5c330f1545b75042338226bba Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 28 Mar 2025 09:33:29 +0100 Subject: [PATCH 074/593] Apply same call history workaround for missed calls count --- .../ui/main/viewmodel/AbstractMainViewModel.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractMainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractMainViewModel.kt index cfd562bb1f..7482d4b92a 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractMainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractMainViewModel.kt @@ -269,7 +269,13 @@ open class AbstractMainViewModel @WorkerThread fun updateMissedCallsCount() { val account = LinphoneUtils.getDefaultAccount() - val count = account?.missedCallsCount ?: coreContext.core.missedCallsCount + // Fetch all call logs if only one account to workaround no history issue + // TODO FIXME: remove workaround later + val count = if (coreContext.core.accountList.size > 1) { + account?.missedCallsCount ?: coreContext.core.missedCallsCount + } else { + coreContext.core.missedCallsCount + } val moreThanOne = count > 1 Log.i( "$TAG There ${if (moreThanOne) "are" else "is"} [$count] missed ${if (moreThanOne) "calls" else "call"}" @@ -292,7 +298,13 @@ open class AbstractMainViewModel fun resetMissedCallsCount() { coreContext.postOnCoreThread { core -> val account = LinphoneUtils.getDefaultAccount() - account?.resetMissedCallsCount() ?: core.resetMissedCallsCount() + // Fetch all call logs if only one account to workaround no history issue + // TODO FIXME: remove workaround later + if (coreContext.core.accountList.size > 1) { + account?.resetMissedCallsCount() ?: core.resetMissedCallsCount() + } else { + core.resetMissedCallsCount() + } updateMissedCallsCount() } } From c35a44b1a0ff78df4159343707348b33317f0a41 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 28 Mar 2025 09:37:03 +0100 Subject: [PATCH 075/593] Fixed migration scenario where logs upload sharing server url might not be set --- app/src/main/java/org/linphone/core/CoreContext.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index e56136b06e..f579693051 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -573,6 +573,11 @@ class CoreContext configurationMigration5To6() } + if (core.logCollectionUploadServerUrl.isNullOrEmpty()) { + Log.w("$TAG Logs sharing server URL not set, fixing that") + core.logCollectionUploadServerUrl = "https://files.linphone.org/http-file-transfer-server/hft.php" + } + corePreferences.linphoneConfigurationVersion = currentVersion Log.w( "$TAG Core configuration updated to version [${corePreferences.linphoneConfigurationVersion}]" From 0eb659b633ea32e6ef36bed387459df8a4085705 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 28 Mar 2025 10:25:14 +0100 Subject: [PATCH 076/593] Updated CHANGELOG from release/6.0 branch --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ app/build.gradle.kts | 6 +++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 788833e9d9..9986841006 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,34 @@ Group changes to describe their impact on the project, as follows: Fixed for any bug fixes. Security to invite users to upgrade in case of vulnerabilities. +## [6.0.2] - 2025-03-28 + +### Added +- Show on top bar if FULL_SCREEN_INTENT permission isn't granted, clicking on it sends to the matching settings so user can fix it easily, without it incoming call screen won't be displayed if screen is off +- Ring during incoming early media call setting added back +- Added a floating action button to open dialpad during outgoing early media call + +### Changed +- Delete all related call history / conversations / meetings when removing an account +- Delay / use a separated thread for heavy contacts related tasks to ensure call is correctly handled and foreground service is started quickly enough +- Newly created account in app will be kept disabled until SMS code validation is done +- Keep app alive foreground service notification no shows a content message to ease clicking on it to open the app & workaround a crash on some devices +- Automatically show dialpad setting will now also work on new / transfer call while in call as well + +### Fixed +- Improved POST_NOTIFICATIONS permission check on Android 13 and newer, should prevent crashes +- Fixed contact lookup if phone number starts by "00" instead of "+" +- Fixed "delete all call history" sometimes not removing all call logs +- Fixed LDAP / remote CardDAV contacts sometimes not displayed in contacts list when doing a search +- Fixed issue where contact filter could be set to only show sip.linphone.org contacts even when third party account was being selected +- Fixed sometimes wrong displayed SIP URI in detailed call history +- Fixed invisible meeting icon in status bar +- Fixed missed call count indicator behavior with some third party providers +- Prevent today indicator & meeting icon in bottom nav bar from blinking / briefly appearing +- Fixed bottom nav bar sometimes being hidden +- Fixed missing share logs server URL when migrating from 5.2 if that value was removed back then +- Other crashes fixed + ## [6.0.1] - 2025-03-21 ### Added diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2cb956c5d8..b2048d39f6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,7 +32,7 @@ if (firebaseCloudMessagingAvailable) { } var gitBranch = ByteArrayOutputStream() -var gitVersion = "6.0.0" +var gitVersion = "6.0.2" task("getGitVersion") { val gitVersionStream = ByteArrayOutputStream() @@ -100,8 +100,8 @@ android { applicationId = packageName minSdk = 28 targetSdk = 35 - versionCode = 600001 // 6.00.000 - versionName = "6.0.1" + versionCode = 600002 // 6.00.002 + versionName = "6.0.2" manifestPlaceholders["appAuthRedirectScheme"] = packageName From 3f3a22984441d49d300a8e2deefe13d080608206 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 28 Mar 2025 20:04:29 +0100 Subject: [PATCH 077/593] Updated Github issue template with 6.0 way of sharing logs --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .idea/misc.xml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 85f3b6ff8e..313652c8eb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -39,7 +39,7 @@ If you are using a SDK that isn't the latest release, please update first as it' 5. **SDK logs** (mandatory) -Enable debug logs in advanced section of the settings, restart the app, reproduce the issue and then go back to advanced settings, click on "Send logs" and copy/paste the link here. +Click on "Share logs" in Help -> Troubleshooting view and copy/paste the link here. It's also explained [in the README](https://github.com/BelledonneCommunications/linphone-android#behavior-issue). diff --git a/.idea/misc.xml b/.idea/misc.xml index 74dd639e4e..b2c751a35c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - From 23810e41e56bc2e283ae7674732feaf1bf30259a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 31 Mar 2025 09:21:38 +0200 Subject: [PATCH 078/593] Forgot to change some of POST_NOTIFICATIONS checks --- .../notifications/NotificationsManager.kt | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 77a8bf5077..787e71073f 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -715,14 +715,10 @@ class NotificationsManager Log.i("$TAG Trying to start foreground Service using incoming call notification") val service = inCallService if (service != null) { - Log.i( - "$TAG Service found, starting it as foreground using notification ID [$INCOMING_CALL_ID] with type PHONE_CALL" - ) - if (ActivityCompat.checkSelfPermission( - context, - Manifest.permission.POST_NOTIFICATIONS - ) == PackageManager.PERMISSION_GRANTED - ) { + if (Compatibility.isPostNotificationsPermissionGranted(context)) { + Log.i( + "$TAG Service found, starting it as foreground using notification ID [$INCOMING_CALL_ID] with type PHONE_CALL" + ) Compatibility.startServiceForeground( service, INCOMING_CALL_ID, @@ -826,11 +822,7 @@ class NotificationsManager } } - if (ActivityCompat.checkSelfPermission( - context, - Manifest.permission.POST_NOTIFICATIONS - ) == PackageManager.PERMISSION_GRANTED - ) { + if (Compatibility.isPostNotificationsPermissionGranted(context)) { Log.i( "$TAG Service found, starting it as foreground using notification ID [${notifiable.notificationId}] with type(s) [$mask]" ) From bd52960749648a75c311e8a7e3cd0734b2b69424 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 31 Mar 2025 12:04:48 +0200 Subject: [PATCH 079/593] Fixed behavior when video is disabled in settings, should not show incoming video calls as video nor route audio to speaker automatically --- app/src/main/java/org/linphone/utils/LinphoneUtils.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 1b6249dd8c..9d53515289 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -227,6 +227,11 @@ class LinphoneUtils { @WorkerThread fun isVideoEnabled(call: Call): Boolean { + if (!call.core.isVideoEnabled) { + Log.w("$TAG Video is disabled in Core, assume call is audio only") + return false + } + val conference = call.conference val isConference = conference != null From bab2acb75c1101313a0d49eb57f80ad490b101fb Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 31 Mar 2025 13:31:19 +0200 Subject: [PATCH 080/593] Prevent meetings list display issue if source isn't sorted --- .../ui/main/meetings/viewmodel/MeetingsListViewModel.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt index 361f9a5e6b..0c514b1471 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt @@ -181,13 +181,17 @@ class MeetingsListViewModel fetchInProgress.postValue(true) } + val sortedSource = source.toList().sortedBy { + it.dateTime + } + val list = arrayListOf() var previousModel: MeetingModel? = null var previousModelWeekLabel = "" var meetingForTodayFound = false - Log.d("$TAG There are [${source.size}] conference info in DB") + Log.d("$TAG There are [${sortedSource.size}] conference info in DB") - for (info: ConferenceInfo in source) { + for (info: ConferenceInfo in sortedSource) { if (info.duration == 0) { Log.d( "$TAG Skipping conference info [${info.subject}] with uri [${info.uri?.asStringUriOnly()}] because it has no duration" From bdb26153007db6a160e78b9e844c62d67055eda2 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 31 Mar 2025 14:15:18 +0200 Subject: [PATCH 081/593] Targetting Android 16 Baklava (API level 36) --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b2048d39f6..27a6e1700e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -94,12 +94,12 @@ project.tasks.preBuild.dependsOn("linphoneSdkSource") android { namespace = "org.linphone" - compileSdk = 35 + compileSdk = 36 defaultConfig { applicationId = packageName minSdk = 28 - targetSdk = 35 + targetSdk = 36 versionCode = 600002 // 6.00.002 versionName = "6.0.2" From 903aaad6fe4d0ff0795c28b894bd6eb20ce26744 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 31 Mar 2025 16:20:37 +0200 Subject: [PATCH 082/593] Do not store friends map in ContactsLoader, might cause concurrent modification --- app/src/main/java/org/linphone/contacts/ContactLoader.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactLoader.kt b/app/src/main/java/org/linphone/contacts/ContactLoader.kt index 5f7f6b9220..c7078ae855 100644 --- a/app/src/main/java/org/linphone/contacts/ContactLoader.kt +++ b/app/src/main/java/org/linphone/contacts/ContactLoader.kt @@ -67,8 +67,6 @@ class ContactLoader : LoaderManager.LoaderCallbacks { private const val MIN_INTERVAL_TO_WAIT_BEFORE_REFRESH = 300000L // 5 minutes } - private val friends = HashMap() - private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @MainThread @@ -173,6 +171,8 @@ class ContactLoader : LoaderManager.LoaderCallbacks { val familyNameColumn = cursor.getColumnIndexOrThrow( ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME ) + + val friends = HashMap() while (!cursor.isClosed && cursor.moveToNext()) { try { val id: String = cursor.getString(contactIdColumn) @@ -275,7 +275,7 @@ class ContactLoader : LoaderManager.LoaderCallbacks { Log.i("$TAG Contacts parsed, posting another task to handle adding them (or not)") // Re-post another task to allow other tasks on Core thread coreContext.postOnCoreThreadWhenAvailableForHeavyTask({ - addFriendsIfNeeded() + addFriendsIfNeeded(friends) }, "add friends to Core") } catch (sde: StaleDataException) { Log.e("$TAG State Data Exception: $sde") @@ -287,7 +287,7 @@ class ContactLoader : LoaderManager.LoaderCallbacks { } @WorkerThread - private fun addFriendsIfNeeded() { + private fun addFriendsIfNeeded(friends: HashMap) { val core = coreContext.core if (core.globalState == GlobalState.Shutdown || core.globalState == GlobalState.Off) { From 80eaf08fbfbe61fa36e152bf9e5b3836920385b6 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 31 Mar 2025 16:20:37 +0200 Subject: [PATCH 083/593] Refresh lists content when going back from background after at least 1 hour (when keep alive service is enabled) --- .../chat/fragment/ConversationsListFragment.kt | 5 +++++ .../ui/main/fragment/AbstractMainFragment.kt | 17 +++++++++++++++++ .../history/fragment/HistoryListFragment.kt | 5 +++++ .../meetings/fragment/MeetingsListFragment.kt | 5 +++++ 4 files changed, 32 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt index 1ef9a7c0ed..862137b472 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt @@ -346,6 +346,11 @@ class ConversationsListFragment : AbstractMainFragment() { } catch (e: IllegalStateException) { Log.e("$TAG Failed to unregister data observer to adapter: $e") } + + if (shouldRefreshDataInOnResume()) { + Log.i("$TAG Keep app alive setting is enabled, refreshing view just in case") + listViewModel.filter() + } } override fun onPause() { diff --git a/app/src/main/java/org/linphone/ui/main/fragment/AbstractMainFragment.kt b/app/src/main/java/org/linphone/ui/main/fragment/AbstractMainFragment.kt index 25e31dcefb..189881a794 100644 --- a/app/src/main/java/org/linphone/ui/main/fragment/AbstractMainFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/fragment/AbstractMainFragment.kt @@ -35,6 +35,7 @@ import androidx.navigation.fragment.findNavController import androidx.slidingpanelayout.widget.SlidingPaneLayout import androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener import com.google.android.material.textfield.TextInputLayout +import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.core.tools.Log import org.linphone.databinding.BottomNavBarBinding @@ -55,6 +56,8 @@ import org.linphone.utils.showKeyboard abstract class AbstractMainFragment : GenericMainFragment() { companion object { private const val TAG = "[Abstract Main Fragment]" + + private const val TIME_MS_AFTER_WHICH_REFRESH_DATA_ON_RESUME = 3600000 // 1 hour } protected val outlineProvider = object : ViewOutlineProvider() { @@ -65,6 +68,8 @@ abstract class AbstractMainFragment : GenericMainFragment() { } } + protected var lastOnPauseTimestamp: Long = -1L + private var currentFragmentId: Int = 0 private lateinit var viewModel: AbstractMainViewModel @@ -96,9 +101,21 @@ abstract class AbstractMainFragment : GenericMainFragment() { backPressedCallback ) + lastOnPauseTimestamp = -1 super.onViewCreated(view, savedInstanceState) } + override fun onPause() { + lastOnPauseTimestamp = System.currentTimeMillis() + super.onPause() + } + + fun shouldRefreshDataInOnResume(): Boolean { + if (lastOnPauseTimestamp == -1L) return false + if (!corePreferences.keepServiceAlive) return false + return System.currentTimeMillis() - lastOnPauseTimestamp > TIME_MS_AFTER_WHICH_REFRESH_DATA_ON_RESUME + } + fun setViewModel(abstractMainViewModel: AbstractMainViewModel) { (view?.parent as? ViewGroup)?.doOnPreDraw { startPostponedEnterTransition() diff --git a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt index c482ae0a44..510a4aa73e 100644 --- a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt @@ -274,6 +274,11 @@ class HistoryListFragment : AbstractMainFragment() { Log.i("$TAG Fragment is resumed, resetting missed calls count") sharedViewModel.resetMissedCallsCountEvent.value = Event(true) sharedViewModel.refreshDrawerMenuAccountsListEvent.value = Event(false) + + if (shouldRefreshDataInOnResume()) { + Log.i("$TAG Keep app alive setting is enabled, refreshing view just in case") + listViewModel.filter() + } } private fun copyNumberOrAddressToClipboard(value: String) { diff --git a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt index 615df62277..2b59e0da00 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt @@ -287,6 +287,11 @@ class MeetingsListFragment : AbstractMainFragment() { Log.e("$TAG Failed to register data observer to adapter: $e") } + if (shouldRefreshDataInOnResume()) { + Log.i("$TAG Keep app alive setting is enabled, refreshing view just in case") + listViewModel.filter() + } + goToContactsIfMeetingsAreDisabledForCurrentlyDefaultAccount() } From 9255830fe2788bea84aaabf6b0a5b1eb66070319 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 2 Apr 2025 14:20:13 +0200 Subject: [PATCH 084/593] Show copy SIP URI icon & do it on click in call history like in conversation details --- .../linphone/ui/main/history/fragment/HistoryFragment.kt | 4 ++++ app/src/main/res/layout/history_fragment.xml | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryFragment.kt b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryFragment.kt index f1f2435162..4fad504486 100644 --- a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryFragment.kt @@ -182,6 +182,10 @@ class HistoryFragment : SlidingPaneChildFragment() { binding.setMenuClickListener { showPopupMenu() } + + binding.setCopyPeerSipUriClickListener { + copyNumberOrAddressToClipboard(viewModel.callLogModel.value?.sipUri.orEmpty()) + } } private fun copyNumberOrAddressToClipboard(value: String) { diff --git a/app/src/main/res/layout/history_fragment.xml b/app/src/main/res/layout/history_fragment.xml index cbc638fa05..417fc230dd 100644 --- a/app/src/main/res/layout/history_fragment.xml +++ b/app/src/main/res/layout/history_fragment.xml @@ -12,6 +12,9 @@ + @@ -120,6 +123,7 @@ From a5872ef8de679a53fbd9e7af2d3f2626965b6659 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 2 Apr 2025 15:15:09 +0200 Subject: [PATCH 085/593] Set default values for notification channels, do not rely only on importance level --- .../linphone/notifications/NotificationsManager.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 787e71073f..7f60c76257 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -1618,6 +1618,7 @@ class NotificationsManager val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH).apply { description = name lockscreenVisibility = Notification.VISIBILITY_PUBLIC + setShowBadge(false) } notificationManager.createNotificationChannel(channel) } @@ -1630,7 +1631,9 @@ class NotificationsManager val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH).apply { description = name lockscreenVisibility = Notification.VISIBILITY_PUBLIC + enableLights(true) enableVibration(true) + setShowBadge(true) } notificationManager.createNotificationChannel(channel) } @@ -1643,6 +1646,9 @@ class NotificationsManager val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW).apply { description = name lockscreenVisibility = Notification.VISIBILITY_PUBLIC + enableLights(false) + enableVibration(false) + setShowBadge(false) } notificationManager.createNotificationChannel(channel) } @@ -1655,7 +1661,9 @@ class NotificationsManager val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH).apply { description = name lockscreenVisibility = Notification.VISIBILITY_PUBLIC + enableLights(true) enableVibration(true) + setShowBadge(true) } notificationManager.createNotificationChannel(channel) } @@ -1667,6 +1675,9 @@ class NotificationsManager val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW).apply { description = context.getString(R.string.notification_channel_service_desc) + enableLights(false) + enableVibration(false) + setShowBadge(false) } notificationManager.createNotificationChannel(channel) } From 06d8e903fccb0d22354e1c822578ee67c7ec2728 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 3 Apr 2025 09:47:22 +0200 Subject: [PATCH 086/593] Revert "Trying to prevent bottom bar from disappearing sometimes", trying better fix instead This reverts commit 317a7c44170777cb7a294ccc0078241f48bb16fc. --- .../ui/main/fragment/AbstractMainFragment.kt | 23 ++++++++++++++----- .../main/res/layout/chat_list_fragment.xml | 5 ++-- .../res/layout/contacts_list_fragment.xml | 7 +++--- .../main/res/layout/history_list_fragment.xml | 5 ++-- .../res/layout/meetings_list_fragment.xml | 5 ++-- 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/fragment/AbstractMainFragment.kt b/app/src/main/java/org/linphone/ui/main/fragment/AbstractMainFragment.kt index 189881a794..e691f515bf 100644 --- a/app/src/main/java/org/linphone/ui/main/fragment/AbstractMainFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/fragment/AbstractMainFragment.kt @@ -72,6 +72,8 @@ abstract class AbstractMainFragment : GenericMainFragment() { private var currentFragmentId: Int = 0 + private lateinit var navigationBar: View + private lateinit var viewModel: AbstractMainViewModel private val backPressedCallback = object : OnBackPressedCallback(false) { @@ -201,9 +203,10 @@ abstract class AbstractMainFragment : GenericMainFragment() { navBar: BottomNavBarBinding, @IdRes fragmentId: Int ) { + navigationBar = navBar.root + initSlidingPane(slidingPane) initSearchBar(topBar.search) - initBottomNavBar(navBar.root) initNavigation(fragmentId) } @@ -228,6 +231,7 @@ abstract class AbstractMainFragment : GenericMainFragment() { it.consume { if (slidingPane.isSlideable) { Log.d("$TAG Closing sliding pane") + ensureNavigationBarIsVisible() slidingPane.closePane() } } @@ -256,7 +260,9 @@ abstract class AbstractMainFragment : GenericMainFragment() { slidingPane.removePanelSlideListener(this) } - override fun onPanelClosed(panel: View) { } + override fun onPanelClosed(panel: View) { + ensureNavigationBarIsVisible() + } }) } slidingPane.openPane() @@ -285,15 +291,20 @@ abstract class AbstractMainFragment : GenericMainFragment() { searchBar.showKeyboard() } else { searchBar.hideKeyboard() + ensureNavigationBarIsVisible() } } } - } - private fun initBottomNavBar(navBar: View) { - view?.setKeyboardInsetListener { keyboardVisible -> + searchBar.setKeyboardInsetListener { keyboardVisible -> val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE - navBar.visibility = if (!portraitOrientation || !keyboardVisible) View.VISIBLE else View.GONE + navigationBar.visibility = if (!portraitOrientation || !keyboardVisible) View.VISIBLE else View.GONE + } + } + + private fun ensureNavigationBarIsVisible() { + if (::navigationBar.isInitialized) { + navigationBar.visibility = View.VISIBLE } } diff --git a/app/src/main/res/layout/chat_list_fragment.xml b/app/src/main/res/layout/chat_list_fragment.xml index b59c17bbc8..ca9901314b 100644 --- a/app/src/main/res/layout/chat_list_fragment.xml +++ b/app/src/main/res/layout/chat_list_fragment.xml @@ -58,9 +58,10 @@ android:id="@+id/conversations_list" android:background="@drawable/shape_squircle_white_r20_top_background" android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_height="0dp" android:layout_marginTop="@dimen/top_bar_height" - android:layout_marginBottom="@dimen/portrait_nav_bar_height" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar" /> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@id/bottom_nav_bar" /> Date: Thu, 3 Apr 2025 10:05:43 +0200 Subject: [PATCH 087/593] Follow contacts list filter for every contact/address picker --- .../java/org/linphone/core/CoreContext.kt | 15 +++++ .../viewmodel/ContactsListViewModel.kt | 36 ++++-------- .../viewmodel/AddressSelectionViewModel.kt | 58 +++++++++---------- .../java/org/linphone/utils/LinphoneUtils.kt | 34 +++++++++++ 4 files changed, 87 insertions(+), 56 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index f579693051..dcfaf6b77c 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -180,6 +180,21 @@ class CoreContext @WorkerThread override fun onDefaultAccountChanged(core: Core, account: Account?) { defaultAccountHasVideoConferenceFactoryUri = account?.params?.audioVideoConferenceFactoryAddress != null + + val defaultDomain = corePreferences.defaultDomain + val isAccountOnDefaultDomain = account?.params?.domain == defaultDomain + val domainFilter = corePreferences.contactsFilter + Log.i("$TAG Currently selected filter is [$domainFilter]") + + if (!isAccountOnDefaultDomain && domainFilter == defaultDomain) { + corePreferences.contactsFilter = "*" + Log.i( + "$TAG New default account isn't on default domain, changing filter to any SIP contacts instead" + ) + } else if (isAccountOnDefaultDomain && domainFilter != "") { + corePreferences.contactsFilter = defaultDomain + Log.i("$TAG New default account is on default domain, using that domain as filter instead of wildcard") + } } @WorkerThread diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt index e4064861b6..e5293ea000 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt @@ -104,7 +104,8 @@ class ContactsListViewModel showFilter.value = !corePreferences.hidePhoneNumbers && !corePreferences.hideSipAddresses coreContext.postOnCoreThread { core -> - updateDomainFilter() + domainFilter = corePreferences.contactsFilter + checkIfDefaultAccountOnDefaultDomain() coreContext.contactsManager.addListener(contactsListener) magicSearch = core.createMagicSearch() @@ -141,7 +142,8 @@ class ContactsListViewModel @UiThread fun applyCurrentDefaultAccountFilter() { coreContext.postOnCoreThread { - updateDomainFilter() + domainFilter = corePreferences.contactsFilter + checkIfDefaultAccountOnDefaultDomain() coreContext.postOnMainThread { applyFilter(currentFilter) @@ -179,28 +181,6 @@ class ContactsListViewModel corePreferences.showFavoriteContacts = show } - @WorkerThread - private fun updateDomainFilter() { - val defaultAccount = coreContext.core.defaultAccount - val defaultDomain = corePreferences.defaultDomain - val isAccountOnDefaultDomain = defaultAccount?.params?.domain == defaultDomain - isDefaultAccountLinphone.postValue(isAccountOnDefaultDomain) - - domainFilter = corePreferences.contactsFilter - Log.i("$TAG Currently selected filter is [$domainFilter]") - if (!isAccountOnDefaultDomain && domainFilter == defaultDomain) { - domainFilter = "*" - corePreferences.contactsFilter = domainFilter - Log.i( - "$TAG New default account isn't on default domain, changing filter to all SIP contacts instead" - ) - } else if (isAccountOnDefaultDomain && domainFilter != "") { - domainFilter = defaultDomain - corePreferences.contactsFilter = domainFilter - Log.i("$TAG New default account is on default domain, using that as filter instead") - } - } - @UiThread fun exportContactAsVCard(friend: Friend) { coreContext.postOnCoreThread { @@ -354,4 +334,12 @@ class ContactsListViewModel Log.i("$TAG Processed [${results.size}] results into [${list.size} contacts]") firstLoad = false } + + @WorkerThread + private fun checkIfDefaultAccountOnDefaultDomain() { + val defaultAccount = coreContext.core.defaultAccount + val defaultDomain = corePreferences.defaultDomain + val isAccountOnDefaultDomain = defaultAccount?.params?.domain == defaultDomain + isDefaultAccountLinphone.postValue(isAccountOnDefaultDomain) + } } diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt index c095029ba7..b4898578f3 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt @@ -38,7 +38,6 @@ import org.linphone.mediastream.Log import org.linphone.ui.main.contacts.model.ContactAvatarModel import org.linphone.ui.main.model.ConversationContactOrSuggestionModel import org.linphone.ui.main.model.SelectedAddressModel -import org.linphone.ui.main.model.isEndToEndEncryptionMandatory import org.linphone.utils.AppUtils import org.linphone.utils.LinphoneUtils @@ -68,8 +67,6 @@ abstract class AddressSelectionViewModel private var currentFilter = "" private var previousFilter = "NotSet" - private var limitSearchToLinphoneAccounts = true - private lateinit var magicSearch: MagicSearch private val magicSearchListener = object : MagicSearchListenerStub() { @@ -86,7 +83,6 @@ abstract class AddressSelectionViewModel Log.i("$TAG Contacts have been (re)loaded, updating list") applyFilter( currentFilter, - if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "", magicSearchSourceFlags ) } @@ -100,8 +96,6 @@ abstract class AddressSelectionViewModel isEmpty.value = true coreContext.postOnCoreThread { core -> - limitSearchToLinphoneAccounts = isEndToEndEncryptionMandatory() - coreContext.contactsManager.addListener(contactsListener) magicSearch = core.createMagicSearch() magicSearch.limitedSearch = true @@ -220,7 +214,6 @@ abstract class AddressSelectionViewModel coreContext.postOnCoreThread { applyFilter( filter, - if (limitSearchToLinphoneAccounts) corePreferences.defaultDomain else "", magicSearchSourceFlags ) } @@ -229,7 +222,6 @@ abstract class AddressSelectionViewModel @WorkerThread private fun applyFilter( filter: String, - domain: String, sources: Int ) { if (previousFilter.isNotEmpty() && ( @@ -242,6 +234,7 @@ abstract class AddressSelectionViewModel currentFilter = filter previousFilter = filter + val domain = corePreferences.contactsFilter Log.i( "$TAG Asking Magic search for contacts matching filter [$filter], domain [$domain] and in sources [$sources]" ) @@ -269,23 +262,14 @@ abstract class AddressSelectionViewModel for (result in results) { val address = result.address - if (address != null) { - if (result.sourceFlags == MagicSearch.Source.Request.toInt()) { - val model = ConversationContactOrSuggestionModel(address) { - coreContext.startAudioCall(address) - } - suggestionsList.add(model) - continue - } - - val friend = result.friend ?: coreContext.contactsManager.findContactByAddress( - address - ) - if (friend != null) { - val found = contactsList.find { it.friend == friend } - if (found != null) continue - - val model = ConversationContactOrSuggestionModel(address, friend = friend) + val friend = result.friend + if (friend != null) { + val found = contactsList.find { it.friend == friend } + if (found != null) continue + + val mainAddress = address ?: LinphoneUtils.getFirstAvailableAddressForFriend(friend) + if (mainAddress != null) { + val model = ConversationContactOrSuggestionModel(mainAddress, friend = friend) val avatarModel = coreContext.contactsManager.getContactAvatarModelForFriend( friend ) @@ -297,18 +281,28 @@ abstract class AddressSelectionViewModel contactsList.add(model) } } else { - val defaultAccountAddress = coreContext.core.defaultAccount?.params?.identityAddress - if (defaultAccountAddress != null && address.weakEqual(defaultAccountAddress)) { - Log.i("$TAG Removing from suggestions current default account address") - continue - } - + Log.w("$TAG Found friend [${friend.name}] in search results but no Address could be found, skipping it") + } + } else if (address != null) { + if (result.sourceFlags == MagicSearch.Source.Request.toInt()) { val model = ConversationContactOrSuggestionModel(address) { coreContext.startAudioCall(address) } - suggestionsList.add(model) + continue } + + val defaultAccountAddress = coreContext.core.defaultAccount?.params?.identityAddress + if (defaultAccountAddress != null && address.weakEqual(defaultAccountAddress)) { + Log.i("$TAG Removing from suggestions current default account address") + continue + } + + val model = ConversationContactOrSuggestionModel(address) { + coreContext.startAudioCall(address) + } + + suggestionsList.add(model) } } diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 9d53515289..24fb71d6b2 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -143,6 +143,40 @@ class LinphoneUtils { return null } + @WorkerThread + fun getFirstAvailableAddressForFriend(friend: Friend): Address? { + // Return any SIP address first + val address = friend.address ?: friend.addresses.firstOrNull() + if (address != null) return address + + val phoneNumbers = friend.phoneNumbers + // If no SIP address stored in Friend, check for SIP address in phone numbers presence + for (phoneNumber in phoneNumbers) { + val presenceModel = friend.getPresenceModelForUriOrTel(phoneNumber) + val hasPresenceInfo = !presenceModel?.contact.isNullOrEmpty() + if (presenceModel != null && hasPresenceInfo) { + val contact = presenceModel.contact + if (!contact.isNullOrEmpty()) { + val address = coreContext.core.interpretUrl(contact, false) + if (address != null) { + address.clean() // To remove ;user=phone + return address + } + } + } + } + + // Finally format any phone number as SIP address + for (phoneNumber in phoneNumbers) { + val address = coreContext.core.interpretUrl(phoneNumber, false) + if (address != null) { + return address + } + } + + return null + } + @AnyThread fun isCallIncoming(callState: Call.State): Boolean { return when (callState) { From a2680028cec6feb3033b123cb81950955db64001 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 3 Apr 2025 11:36:50 +0200 Subject: [PATCH 088/593] Keep attach file icon when keyboard is opened in chat instead of emoji picker --- app/src/main/res/layout/chat_conversation_send_area.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/chat_conversation_send_area.xml b/app/src/main/res/layout/chat_conversation_send_area.xml index 0d961eb88f..13339b0c44 100644 --- a/app/src/main/res/layout/chat_conversation_send_area.xml +++ b/app/src/main/res/layout/chat_conversation_send_area.xml @@ -93,6 +93,7 @@ android:padding="8dp" android:contentDescription="@string/content_description_chat_open_emoji_picker" android:src="@{viewModel.isEmojiPickerOpen ? @drawable/x : @drawable/smiley, default=@drawable/smiley}" + android:visibility="@{viewModel.isVoiceRecording ? View.INVISIBLE : (viewModel.isKeyboardOpen && viewModel.isFileTransferServerAvailable) ? View.GONE : View.VISIBLE}" app:layout_constraintBottom_toBottomOf="@id/message_area_background" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/message_area_background" @@ -108,7 +109,7 @@ android:padding="8dp" android:contentDescription="@string/content_description_chat_open_attach_file" android:src="@{viewModel.areFilePickersOpen ? @drawable/x : @drawable/paperclip, default=@drawable/paperclip}" - android:visibility="@{viewModel.isVoiceRecording ? View.INVISIBLE : (viewModel.isKeyboardOpen || !viewModel.isFileTransferServerAvailable) ? View.GONE : View.VISIBLE}" + android:visibility="@{viewModel.isVoiceRecording ? View.INVISIBLE : !viewModel.isFileTransferServerAvailable ? View.GONE : View.VISIBLE}" app:layout_constraintBottom_toBottomOf="@id/message_area_background" app:layout_constraintEnd_toStartOf="@id/message_area_background" app:layout_constraintStart_toEndOf="@id/emoji_picker_toggle" From 836deaae99c98e5d137a4f5d77bfc181c1b8bd70 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 3 Apr 2025 13:06:49 +0200 Subject: [PATCH 089/593] Fixed no default account issue when removing currently default one --- .../main/java/org/linphone/core/CoreContext.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index dcfaf6b77c..ca9d358a7b 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -472,6 +472,22 @@ class CoreContext Log.i("$TAG Removed account matches auth info pending password update, removing dialog") clearAuthenticationRequestDialogEvent.postValue(Event(true)) } + + if (core.defaultAccount == null || core.defaultAccount == account) { + Log.w("$TAG Removed account was the default one, choosing another as default if possible") + val newDefaultAccount = core.accountList.find { + it.params.isRegisterEnabled == true + } ?: core.accountList.firstOrNull() + if (newDefaultAccount == null) { + Log.e("$TAG Failed to find a new default account!") + } else { + Log.i("$TAG New default account will be [${newDefaultAccount.params.identityAddress?.asStringUriOnly()}]") + // Delay changing default account to allow for other onAccountRemoved listeners to trigger first + postOnCoreThread { + core.defaultAccount = newDefaultAccount + } + } + } } } From 5e9be7d10b22473a6761cb23e9ff4d881f48dfdb Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 3 Apr 2025 14:52:16 +0200 Subject: [PATCH 090/593] Show alert when default account is disabled --- .../java/org/linphone/ui/main/MainActivity.kt | 15 ---- .../ui/main/viewmodel/MainViewModel.kt | 82 ++++++++++++------- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 56 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/MainActivity.kt b/app/src/main/java/org/linphone/ui/main/MainActivity.kt index f8effd852c..b953aa161e 100644 --- a/app/src/main/java/org/linphone/ui/main/MainActivity.kt +++ b/app/src/main/java/org/linphone/ui/main/MainActivity.kt @@ -227,21 +227,6 @@ class MainActivity : GenericActivity() { } } - viewModel.defaultAccountRegistrationErrorEvent.observe(this) { - it.consume { error -> - val tag = "DEFAULT_ACCOUNT_REGISTRATION_ERROR" - if (error) { - // First remove any already existing connection error toast - removePersistentRedToast(tag) - - val message = getString(R.string.default_account_connection_state_error_toast) - showPersistentRedToast(message, R.drawable.warning_circle, tag) - } else { - removePersistentRedToast(tag) - } - } - } - viewModel.showNewAccountToastEvent.observe(this) { it.consume { val message = getString(R.string.new_account_configured_toast) diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt index b6ff5e1665..c4f780d4e8 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt @@ -58,8 +58,9 @@ class MainViewModel const val MWI_MESSAGES_WAITING = 4 const val NON_DEFAULT_ACCOUNT_NOTIFICATIONS = 5 const val NON_DEFAULT_ACCOUNT_NOT_CONNECTED = 10 - const val FULL_SCREEN_INTENTS_PERMISSION_NOT_GRANTED = 16 - const val SEND_NOTIFICATIONS_PERMISSION_NOT_GRANTED = 17 + const val FULL_SCREEN_INTENTS_PERMISSION_NOT_GRANTED = 14 + const val SEND_NOTIFICATIONS_PERMISSION_NOT_GRANTED = 15 + const val DEFAULT_ACCOUNT_DISABLED = 18 const val NETWORK_NOT_REACHABLE = 19 const val SINGLE_CALL = 20 const val MULTIPLE_CALLS = 21 @@ -77,10 +78,6 @@ class MainViewModel val callsStatus = MutableLiveData() - val defaultAccountRegistrationErrorEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - val goBackToCallEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -113,8 +110,6 @@ class MainViewModel var mainIntentHandled = false - private var defaultAccountRegistrationFailed = false - private val alertsList = arrayListOf>() private var firstAccountRegistered: Boolean = false @@ -212,8 +207,10 @@ class MainViewModel RegistrationState.Failed -> { if (account == core.defaultAccount) { Log.e("$TAG Default account registration failed!") - defaultAccountRegistrationFailed = true - defaultAccountRegistrationErrorEvent.postValue(Event(true)) + val label = AppUtils.getString( + R.string.connection_error_for_non_default_account + ) + addAlert(DEFAULT_ACCOUNT_DISABLED, label) } else if (core.isNetworkReachable) { Log.e("$TAG Non-default account registration failed!") val label = AppUtils.getString( @@ -230,11 +227,8 @@ class MainViewModel } if (account == core.defaultAccount) { - if (defaultAccountRegistrationFailed) { - Log.i("$TAG Default account is now registered") - defaultAccountRegistrationFailed = false - defaultAccountRegistrationErrorEvent.postValue(Event(false)) - } + Log.i("$TAG Default account is now registered") + removeAlert(DEFAULT_ACCOUNT_DISABLED) } else { // If no call and no account is in Failed state, hide top bar val found = core.accountList.find { @@ -246,12 +240,20 @@ class MainViewModel } } RegistrationState.Progress, RegistrationState.Refreshing -> { - if (defaultAccountRegistrationFailed) { + if (account == core.defaultAccount) { Log.i( - "$TAG Default account is registering, removing registration failed toast for now" + "$TAG Default account is registering, removing registration failed alert for now" ) - defaultAccountRegistrationFailed = false - defaultAccountRegistrationErrorEvent.postValue(Event(false)) + removeAlert(DEFAULT_ACCOUNT_DISABLED) + } + } + RegistrationState.Cleared -> { + if (account == core.defaultAccount) { + Log.w("$TAG Default account is now disabled") + val label = AppUtils.getString( + R.string.default_account_disabled + ) + addAlert(DEFAULT_ACCOUNT_DISABLED, label) } } else -> {} @@ -271,9 +273,17 @@ class MainViewModel ) coreContext.updateFriendListsSubscriptionDependingOnDefaultAccount() + removeAlert(DEFAULT_ACCOUNT_DISABLED) removeAlert(NON_DEFAULT_ACCOUNT_NOT_CONNECTED) // Refresh REGISTER to re-compute alerts regarding accounts registration state core.refreshRegisters() + + if (!account.params.isRegisterEnabled) { + val label = AppUtils.getString( + R.string.default_account_disabled + ) + addAlert(DEFAULT_ACCOUNT_DISABLED, label) + } } computeNonDefaultAccountNotificationsCount() @@ -287,8 +297,11 @@ class MainViewModel Log.w( "$TAG Account [${account.params.identityAddress?.asStringUriOnly()}] has been removed!" ) + removeAlert(DEFAULT_ACCOUNT_DISABLED) removeAlert(NON_DEFAULT_ACCOUNT_NOT_CONNECTED) + // Refresh REGISTER to re-compute alerts regarding accounts registration state core.refreshRegisters() + computeNonDefaultAccountNotificationsCount() if (core.accountList.isEmpty()) { @@ -325,7 +338,6 @@ class MainViewModel } init { - defaultAccountRegistrationFailed = false showAlert.value = false atLeastOneCall.value = false maxAlertLevel.value = NONE @@ -348,8 +360,18 @@ class MainViewModel atLeastOneCall.postValue(true) } - if (core.defaultAccount?.state == RegistrationState.Ok && !firstAccountRegistered) { - triggerNativeAddressBookImport() + val defaultAccount = core.defaultAccount + if (defaultAccount != null) { + if (!defaultAccount.params.isRegisterEnabled) { + val label = AppUtils.getString( + R.string.default_account_disabled + ) + addAlert(DEFAULT_ACCOUNT_DISABLED, label) + } + + if (defaultAccount.state == RegistrationState.Ok && !firstAccountRegistered) { + triggerNativeAddressBookImport() + } } } @@ -557,20 +579,24 @@ class MainViewModel val label = maxedPriorityAlert.second Log.i("$TAG Max priority alert right now is [$type]") maxAlertLevel.postValue(type) - when (type) { - NON_DEFAULT_ACCOUNT_NOTIFICATIONS, NON_DEFAULT_ACCOUNT_NOT_CONNECTED -> { - alertIcon.postValue(R.drawable.bell_simple) + val icon = when (type) { + DEFAULT_ACCOUNT_DISABLED -> { + R.drawable.warning_circle } NETWORK_NOT_REACHABLE -> { - alertIcon.postValue(R.drawable.wifi_slash) + R.drawable.wifi_slash } SEND_NOTIFICATIONS_PERMISSION_NOT_GRANTED, FULL_SCREEN_INTENTS_PERMISSION_NOT_GRANTED -> { - alertIcon.postValue(R.drawable.bell_simple_slash) + R.drawable.bell_simple_slash } SINGLE_CALL, MULTIPLE_CALLS -> { - alertIcon.postValue(R.drawable.phone) + R.drawable.phone + } + else -> { + R.drawable.bell_simple } } + alertIcon.postValue(icon) alertLabel.postValue(label) if (type < SINGLE_CALL) { diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 188f94e567..2a5e3045b5 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -784,6 +784,7 @@ %s notification en attente %s notifications en attente + Le compte selectionné est désactivé Vous n\'êtes pas connecté à internet Mode Wi-Fi uniquement activé Opération en cours, merci de patienter… diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dcc2eb60a7..938e100d17 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -825,6 +825,7 @@ %s notification for other account(s) %s notifications for other account(s) + Selected account is currently disabled You aren\'t connected to internet Wi-Fi only mode enabled Operation in progress, please wait From dbca62bea985f423e203336ea513d3836ef1b9db Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 3 Apr 2025 17:21:43 +0200 Subject: [PATCH 091/593] Added hidden developer settings --- .../java/org/linphone/core/CorePreferences.kt | 11 ++ .../ui/main/help/viewmodel/HelpViewModel.kt | 21 +++ .../fragment/SettingsDeveloperFragment.kt | 63 +++++++++ .../settings/fragment/SettingsFragment.kt | 10 +- .../settings/viewmodel/SettingsViewModel.kt | 50 +++++-- app/src/main/res/layout/help_fragment.xml | 3 +- .../res/layout/settings_advanced_calls.xml | 30 +---- .../layout/settings_developer_fragment.xml | 123 ++++++++++++++++++ app/src/main/res/layout/settings_fragment.xml | 25 +++- .../main/res/navigation/main_nav_graph.xml | 14 ++ app/src/main/res/values-fr/strings.xml | 5 + app/src/main/res/values/strings.xml | 5 + 12 files changed, 316 insertions(+), 44 deletions(-) create mode 100644 app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsDeveloperFragment.kt create mode 100644 app/src/main/res/layout/settings_developer_fragment.xml diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 9a3718ecdb..99b9da0512 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -109,6 +109,13 @@ class CorePreferences config.setString("app", "device", value.trim()) } + @get:WorkerThread @set:WorkerThread + var showDeveloperSettings: Boolean + get() = config.getBool("ui", "show_developer_settings", false) + set(value) { + config.setBool("ui", "show_developer_settings", value) + } + // Call settings // This won't be done if bluetooth or wired headset is used @@ -317,6 +324,10 @@ class CorePreferences val hideAccountSettings: Boolean get() = config.getBool("ui", "hide_account_settings", false) + @get:WorkerThread + val hideAdvancedSettings: Boolean + get() = config.getBool("ui", "hide_advanced_settings", false) + @get:WorkerThread val hideAssistantCreateAccount: Boolean get() = config.getBool("ui", "assistant_hide_create_account", false) diff --git a/app/src/main/java/org/linphone/ui/main/help/viewmodel/HelpViewModel.kt b/app/src/main/java/org/linphone/ui/main/help/viewmodel/HelpViewModel.kt index 52b39ee7f7..32dab37fe5 100644 --- a/app/src/main/java/org/linphone/ui/main/help/viewmodel/HelpViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/help/viewmodel/HelpViewModel.kt @@ -63,6 +63,8 @@ class HelpViewModel val logsUploadInProgress = MutableLiveData() + val versionClickCount = MutableLiveData() + val newVersionAvailableEvent: MutableLiveData>> by lazy { MutableLiveData>>() } @@ -137,6 +139,7 @@ class HelpViewModel init { val currentVersion = BuildConfig.VERSION_NAME version.value = currentVersion + versionClickCount.value = 0 val versionCode = BuildConfig.VERSION_CODE val appGitDescribe = AppUtils.getString(R.string.linphone_app_version) @@ -166,6 +169,24 @@ class HelpViewModel } } + @UiThread + fun versionClicked() { + if (corePreferences.showDeveloperSettings == true) { + showRedToast(R.string.settings_developer_already_enabled_toast, R.drawable.warning_circle) + return + } + + versionClickCount.value = (versionClickCount.value ?: 0) + 1 + if (versionClickCount.value == 7) { + coreContext.postOnCoreThread { + Log.w("$TAG Version was clicked seven times, enabling developer settings") + corePreferences.showDeveloperSettings = true + + showGreenToast(R.string.settings_developer_enabled_toast, R.drawable.gear) + } + } + } + @UiThread fun toggleLogcat() { val newValue = logcat.value == false diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsDeveloperFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsDeveloperFragment.kt new file mode 100644 index 0000000000..36556bee07 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsDeveloperFragment.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2010-2025 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.ui.main.settings.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.UiThread +import androidx.lifecycle.ViewModelProvider +import org.linphone.databinding.SettingsDeveloperFragmentBinding +import org.linphone.ui.main.fragment.GenericMainFragment +import org.linphone.ui.main.settings.viewmodel.SettingsViewModel + +@UiThread +class SettingsDeveloperFragment : GenericMainFragment() { + private lateinit var binding: SettingsDeveloperFragmentBinding + + private lateinit var viewModel: SettingsViewModel + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = SettingsDeveloperFragmentBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + postponeEnterTransition() + super.onViewCreated(view, savedInstanceState) + + viewModel = ViewModelProvider(this)[SettingsViewModel::class.java] + + binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel + observeToastEvents(viewModel) + + binding.setBackClickListener { + goBack() + } + + startPostponedEnterTransition() + } +} diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt index a521407faf..b3c1afa9ee 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt @@ -105,7 +105,7 @@ class SettingsFragment : GenericMainFragment() { val label = viewModel.availableColorsNames[position] val value = viewModel.availableColorsValues[position] Log.i("$TAG Selected color is now [$label] ($value)") - // Be carefull not to create an infinite loop + // Be careful not to create an infinite loop if (value != viewModel.color.value.orEmpty()) { viewModel.setColor(value) requireActivity().recreate() @@ -155,6 +155,13 @@ class SettingsFragment : GenericMainFragment() { } } + binding.setDeveloperSettingsClickListener { + if (findNavController().currentDestination?.id == R.id.settingsFragment) { + val action = SettingsFragmentDirections.actionSettingsFragmentToSettingsDeveloperFragment() + findNavController().navigate(action) + } + } + viewModel.recreateActivityEvent.observe(viewLifecycleOwner) { it.consume { Log.w("$TAG Recreate Activity") @@ -344,6 +351,7 @@ class SettingsFragment : GenericMainFragment() { viewModel.reloadLdapServers() viewModel.reloadConfiguredCardDavServers() + viewModel.reloadShowDeveloperSettings() } override fun onPause() { diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 4324b19e29..4b6ce43b03 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -195,6 +195,8 @@ class SettingsViewModel ) // Advanced settings + val showAdvancedSettings = MutableLiveData() + val startAtBoot = MutableLiveData() val keepAliveThirdPartyAccountsService = MutableLiveData() val useSmffForCallRecording = MutableLiveData() @@ -209,7 +211,6 @@ class SettingsViewModel val mediaEncryptionLabels = arrayListOf() private val mediaEncryptionValues = arrayListOf() val mediaEncryptionMandatory = MutableLiveData() - val createEndToEndEncryptedConferences = MutableLiveData() val acceptEarlyMedia = MutableLiveData() val ringDuringEarlyMedia = MutableLiveData() val allowOutgoingEarlyMedia = MutableLiveData() @@ -230,6 +231,11 @@ class SettingsViewModel val expandVideoCodecs = MutableLiveData() val videoCodecs = MutableLiveData>() + // Developer settings + val showDeveloperSettings = MutableLiveData() + + val createEndToEndEncryptedConferences = MutableLiveData() + private val coreListener = object : CoreListenerStub() { @WorkerThread override fun onAudioDevicesListUpdated(core: Core) { @@ -256,6 +262,8 @@ class SettingsViewModel ldapAvailable.postValue(core.ldapAvailable()) showThemeSelector.postValue(corePreferences.darkModeAllowed) showColorSelector.postValue(corePreferences.changeMainColorAllowed) + showAdvancedSettings.postValue(!corePreferences.hideAdvancedSettings) + showDeveloperSettings.postValue(corePreferences.showDeveloperSettings) } showContactsSettings.value = true @@ -337,7 +345,6 @@ class SettingsViewModel fileSharingServerUrl.postValue(core.fileTransferServer) remoteProvisioningUrl.postValue(core.provisioningUri) - createEndToEndEncryptedConferences.postValue(corePreferences.createEndToEndEncryptedMeetingsAndGroupCalls) acceptEarlyMedia.postValue(corePreferences.acceptEarlyMedia) ringDuringEarlyMedia.postValue(core.ringDuringIncomingEarlyMedia) allowOutgoingEarlyMedia.postValue(corePreferences.allowOutgoingEarlyMedia) @@ -347,6 +354,8 @@ class SettingsViewModel setupMediaEncryption() setupAudioDevices() setupCodecs() + + createEndToEndEncryptedConferences.postValue(corePreferences.createEndToEndEncryptedMeetingsAndGroupCalls) } } @@ -808,16 +817,6 @@ class SettingsViewModel } } - @UiThread - fun toggleConferencesEndToEndEncryption() { - val newValue = createEndToEndEncryptedConferences.value == false - - coreContext.postOnCoreThread { core -> - corePreferences.createEndToEndEncryptedMeetingsAndGroupCalls = newValue - createEndToEndEncryptedConferences.postValue(newValue) - } - } - @UiThread fun toggleAcceptEarlyMedia() { val newValue = acceptEarlyMedia.value == false @@ -1047,4 +1046,31 @@ class SettingsViewModel } calibratedEchoCancellerValue.postValue(value) } + + @UiThread + fun toggleDeveloperSettings() { + val newValue = showDeveloperSettings.value == false + + coreContext.postOnCoreThread { core -> + corePreferences.showDeveloperSettings = newValue + showDeveloperSettings.postValue(newValue) + } + } + + @UiThread + fun reloadShowDeveloperSettings() { + coreContext.postOnCoreThread { + showDeveloperSettings.postValue(corePreferences.showDeveloperSettings) + } + } + + @UiThread + fun toggleConferencesEndToEndEncryption() { + val newValue = createEndToEndEncryptedConferences.value == false + + coreContext.postOnCoreThread { core -> + corePreferences.createEndToEndEncryptedMeetingsAndGroupCalls = newValue + createEndToEndEncryptedConferences.postValue(newValue) + } + } } diff --git a/app/src/main/res/layout/help_fragment.xml b/app/src/main/res/layout/help_fragment.xml index a74dd4d4c6..f8fd0e3121 100644 --- a/app/src/main/res/layout/help_fragment.xml +++ b/app/src/main/res/layout/help_fragment.xml @@ -144,7 +144,8 @@ diff --git a/app/src/main/res/layout/settings_advanced_calls.xml b/app/src/main/res/layout/settings_advanced_calls.xml index bac6ae83ea..241c37a2a9 100644 --- a/app/src/main/res/layout/settings_advanced_calls.xml +++ b/app/src/main/res/layout/settings_advanced_calls.xml @@ -167,34 +167,6 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/media_encryption" /> - - - - + app:layout_constraintTop_toBottomOf="@id/media_encryption_mandatory_switch" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 81a23ef108..c9515b7597 100644 --- a/app/src/main/res/layout/settings_fragment.xml +++ b/app/src/main/res/layout/settings_fragment.xml @@ -14,6 +14,9 @@ + @@ -315,10 +318,30 @@ android:text="@string/settings_advanced_title" android:drawableEnd="@drawable/caret_right" android:drawableTint="?attr/color_main2_600" + android:visibility="@{viewModel.showAdvancedSettings ? View.VISIBLE : View.GONE}" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/tunnel_settings"/> + + diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml index 5d223456ce..9684ce3f31 100644 --- a/app/src/main/res/navigation/main_nav_graph.xml +++ b/app/src/main/res/navigation/main_nav_graph.xml @@ -137,6 +137,14 @@ app:exitAnim="@anim/slide_out_left" app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" /> + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2a5e3045b5..f58ba37ff1 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -290,6 +290,11 @@ Codecs vidéo Paramètres Android de &appName; + Paramètres développeurs + Afficher les paramètres développeurs + Paramètres développeurs activés + Paramètres développeurs déjà activés + Mon compte Détails diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 938e100d17..e7e453a109 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -330,6 +330,11 @@ Video codecs &appName; Android settings + Developer settings + Show developer settings + Developer settings enabled + Developer settings already enabled + Manage account Details From 8577571e67251996e3b968786662a7f6abc98bdb Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 4 Apr 2025 10:02:30 +0200 Subject: [PATCH 092/593] Show operation in progress during contact search --- .../main/contacts/viewmodel/ContactsListViewModel.kt | 4 ++++ .../ui/main/viewmodel/AddressSelectionViewModel.kt | 5 +++++ .../main/res/layout-land/contacts_list_fragment.xml | 2 +- app/src/main/res/layout/call_transfer_fragment.xml | 12 ++++++++++++ .../res/layout/chat_message_forward_fragment.xml | 12 ++++++++++++ app/src/main/res/layout/contacts_list_fragment.xml | 2 +- .../res/layout/generic_add_participants_fragment.xml | 12 ++++++++++++ app/src/main/res/layout/start_call_fragment.xml | 12 ++++++++++++ app/src/main/res/layout/start_chat_fragment.xml | 12 ++++++++++++ 9 files changed, 71 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt index e5293ea000..5367c67d2e 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt @@ -61,6 +61,8 @@ class ContactsListViewModel val isListFiltered = MutableLiveData() + val searchInProgress = MutableLiveData() + val isDefaultAccountLinphone = MutableLiveData() val vCardTerminatedEvent: MutableLiveData>> by lazy { @@ -255,6 +257,7 @@ class ContactsListViewModel Log.i( "$TAG Asking Magic search for contacts matching filter [$filter], domain [$domain] and in sources Friends/LDAP/CardDAV" ) + searchInProgress.postValue(filter.isNotEmpty()) magicSearch.getContactsListAsync( filter, domain, @@ -328,6 +331,7 @@ class ContactsListViewModel collator.compare(model1.getNameToUseForSorting(), model2.getNameToUseForSorting()) } + searchInProgress.postValue(false) favourites.postValue(favouritesList) contactsList.postValue(list) diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt index b4898578f3..734e9b64dc 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt @@ -56,6 +56,8 @@ abstract class AddressSelectionViewModel val searchFilter = MutableLiveData() + val searchInProgress = MutableLiveData() + val modelsList = MutableLiveData>() val isEmpty = MutableLiveData() @@ -238,6 +240,7 @@ abstract class AddressSelectionViewModel Log.i( "$TAG Asking Magic search for contacts matching filter [$filter], domain [$domain] and in sources [$sources]" ) + searchInProgress.postValue(filter.isNotEmpty()) magicSearch.getContactsListAsync( filter, domain, @@ -322,6 +325,8 @@ abstract class AddressSelectionViewModel list.addAll(favoritesList) list.addAll(contactsList) list.addAll(suggestionsList) + + searchInProgress.postValue(false) modelsList.postValue(list) isEmpty.postValue(list.isEmpty()) Log.i( diff --git a/app/src/main/res/layout-land/contacts_list_fragment.xml b/app/src/main/res/layout-land/contacts_list_fragment.xml index ff56cf2703..9f05a9b613 100644 --- a/app/src/main/res/layout-land/contacts_list_fragment.xml +++ b/app/src/main/res/layout-land/contacts_list_fragment.xml @@ -141,7 +141,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:indeterminate="true" - android:visibility="@{viewModel.fetchInProgress ? View.VISIBLE : View.GONE}" + android:visibility="@{viewModel.fetchInProgress || viewModel.searchInProgress ? View.VISIBLE : View.GONE}" app:indicatorColor="?attr/color_main1_500" app:layout_constraintStart_toEndOf="@id/bottom_nav_bar" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/call_transfer_fragment.xml b/app/src/main/res/layout/call_transfer_fragment.xml index 4e48c82b1c..d63a24017d 100644 --- a/app/src/main/res/layout/call_transfer_fragment.xml +++ b/app/src/main/res/layout/call_transfer_fragment.xml @@ -202,6 +202,18 @@ app:layout_constraintTop_toBottomOf="@id/search_bar" app:layout_constraintBottom_toBottomOf="parent" /> + + + + + + + + + + Date: Fri, 4 Apr 2025 12:59:34 +0200 Subject: [PATCH 093/593] Updated CHANGELOG & version code from release/6.0 branch --- CHANGELOG.md | 22 ++++++++++++++++++++++ app/build.gradle.kts | 6 +++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9986841006..4acc2b8fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,28 @@ Group changes to describe their impact on the project, as follows: Fixed for any bug fixes. Security to invite users to upgrade in case of vulnerabilities. +## [6.0.3] - 2025-04-04 + +### Added +- Show alert when default account is disabled +- Refesh list details when going back from background after one hour or more (when keep app alive using service is enabled) +- Click to copy SIP URI in call history shortcut +- Added developper settings, must click 8 times on version (in Help) to make it appear (E2E encryption for meetings & group calls setting was moved there) +- Circular indicator while search is in progress in contacts lists + +### Changed +- Force some default values on notifications channels +- Contacts list filter is now applied to new call / conversation & other contact pickers +- Attach file icon stays visible while typing message in conversation instead of emoji picker icon + +### Fixed +- No default account being selected if the default one is removed +- Navigation bar turning orange when opening search bar +- Incoming call showed as video even if video is disabled locally +- Concurrent modification crash in Contacts loader +- Meetings list not properly sorted when CCMP is used +- POST_NOTIFICATIONS permission check on old Android devices + ## [6.0.2] - 2025-03-28 ### Added diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 27a6e1700e..970897087d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,7 +32,7 @@ if (firebaseCloudMessagingAvailable) { } var gitBranch = ByteArrayOutputStream() -var gitVersion = "6.0.2" +var gitVersion = "6.0.3" task("getGitVersion") { val gitVersionStream = ByteArrayOutputStream() @@ -100,8 +100,8 @@ android { applicationId = packageName minSdk = 28 targetSdk = 36 - versionCode = 600002 // 6.00.002 - versionName = "6.0.2" + versionCode = 600003 // 6.00.003 + versionName = "6.0.3" manifestPlaceholders["appAuthRedirectScheme"] = packageName From c08157b659cfb69b2c93dc017001c6a913ba63a1 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 3 Apr 2025 16:53:01 +0200 Subject: [PATCH 094/593] Removed code no longer needed, done by SDK now + prevent onContactsLoaded() callback to be triggered too many times when fetching multiple addresses from remote contacts directories --- .../org/linphone/contacts/ContactsManager.kt | 43 ++++++++++--------- .../viewmodel/ContactsListViewModel.kt | 8 ---- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 096698d922..5950197572 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -74,7 +74,7 @@ class ContactsManager private const val TAG = "[Contacts Manager]" private const val DELAY_BEFORE_RELOADING_CONTACTS_AFTER_PRESENCE_RECEIVED = 1000L // 1 second - private const val FRIEND_LIST_TEMPORARY_STORED_NATIVE = "TempNativeContacts" + private const val DELAY_BEFORE_RELOADING_CONTACTS_AFTER_MAGIC_SEARCH_RESULT = 1000L // 1 second private const val FRIEND_LIST_TEMPORARY_STORED_REMOTE_DIRECTORY = "TempRemoteDirectoryContacts" } @@ -90,13 +90,16 @@ class ContactsManager private val unknownRemoteContactDirectoriesContactsMap = arrayListOf() private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) - private var reloadContactsJob: Job? = null + private var reloadPresenceContactsJob: Job? = null + private var reloadRemoteContactsJob: Job? = null private var loadContactsOnlyFromDefaultDirectory = true private val magicSearchListener = object : MagicSearchListenerStub() { @WorkerThread override fun onSearchResultsReceived(magicSearch: MagicSearch) { + reloadRemoteContactsJob?.cancel() + val results = magicSearch.lastSearch Log.i("$TAG [${results.size}] magic search results available") @@ -111,7 +114,7 @@ class ContactsManager found = true // Store friend in app's cache to be re-used in call history, conversations, etc... - val temporaryFriendList = getTemporaryFriendList(native = false) + val temporaryFriendList = getRemoteContactDirectoriesCacheFriendList() temporaryFriendList.addFriend(friend) newContactAdded(friend) Log.i( @@ -121,6 +124,17 @@ class ContactsManager for (listener in listeners) { listener.onContactFoundInRemoteDirectory(friend) } + + reloadRemoteContactsJob = coroutineScope.launch { + delay(DELAY_BEFORE_RELOADING_CONTACTS_AFTER_MAGIC_SEARCH_RESULT) + coreContext.postOnCoreThread { + Log.i("$TAG At least a new SIP address was discovered, reloading contacts") + conferenceAvatarMap.values.forEach(ContactAvatarModel::destroy) + conferenceAvatarMap.clear() + + notifyContactsListChanged() + } + } } } @@ -158,7 +172,7 @@ class ContactsManager friend: Friend, sipUri: String ) { - reloadContactsJob?.cancel() + reloadPresenceContactsJob?.cancel() Log.d( "$TAG Newly discovered SIP Address [$sipUri] for friend [${friend.name}] in list [${friendList.displayName}]" ) @@ -174,7 +188,7 @@ class ContactsManager Log.e("$TAG Failed to parse SIP URI [$sipUri] as Address!") } - reloadContactsJob = coroutineScope.launch { + reloadPresenceContactsJob = coroutineScope.launch { delay(DELAY_BEFORE_RELOADING_CONTACTS_AFTER_PRESENCE_RECEIVED) coreContext.postOnCoreThread { Log.i("$TAG At least a new SIP address was discovered, reloading contacts") @@ -334,10 +348,6 @@ class ContactsManager for (sipAddress in friend.addresses) { newContactAddedWithSipUri(friend, sipAddress.asStringUriOnly()) } - - conferenceAvatarMap.values.forEach(ContactAvatarModel::destroy) - conferenceAvatarMap.clear() - notifyContactsListChanged() } @WorkerThread @@ -370,14 +380,6 @@ class ContactsManager nativeContactsLoaded = true Log.i("$TAG Native contacts have been loaded, cleaning avatars maps") - val core = coreContext.core - val found = getTemporaryFriendList(native = true) - val count = found.friends.size - Log.i( - "$TAG Found temporary friend list with [$count] friends, removing it as no longer necessary" - ) - core.removeFriendList(found) - knownContactsAvatarsMap.values.forEach(ContactAvatarModel::destroy) knownContactsAvatarsMap.clear() unknownContactsAvatarsMap.values.forEach(ContactAvatarModel::destroy) @@ -579,7 +581,7 @@ class ContactsManager fun isContactTemporary(friend: Friend, allowNullFriendList: Boolean = false): Boolean { val friendList = friend.friendList if (friendList == null && !allowNullFriendList) return true - return friendList?.displayName == FRIEND_LIST_TEMPORARY_STORED_NATIVE || friendList?.displayName == FRIEND_LIST_TEMPORARY_STORED_REMOTE_DIRECTORY + return friendList?.type == FriendList.Type.ApplicationCache } @WorkerThread @@ -626,13 +628,14 @@ class ContactsManager } @WorkerThread - fun getTemporaryFriendList(native: Boolean): FriendList { + fun getRemoteContactDirectoriesCacheFriendList(): FriendList { val core = coreContext.core - val name = if (native) FRIEND_LIST_TEMPORARY_STORED_NATIVE else FRIEND_LIST_TEMPORARY_STORED_REMOTE_DIRECTORY + val name = FRIEND_LIST_TEMPORARY_STORED_REMOTE_DIRECTORY val temporaryFriendList = core.getFriendListByName(name) ?: core.createFriendList() if (temporaryFriendList.displayName.isNullOrEmpty()) { temporaryFriendList.isDatabaseStorageEnabled = false temporaryFriendList.displayName = name + temporaryFriendList.type = FriendList.Type.ApplicationCache core.addFriendList(temporaryFriendList) Log.i( "$TAG Created temporary friend list with name [$name]" diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt index 5367c67d2e..aa036ef4a1 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt @@ -279,14 +279,6 @@ class ContactsListViewModel for (result in results) { val friend = result.friend if (friend != null) { - val isFromRemoteDirectory = result.hasSourceFlag(MagicSearch.Source.LdapServers) || result.hasSourceFlag(MagicSearch.Source.RemoteCardDAV) - // Only display friends from temporary friend lists if their source flag show they - // were fetched from a remote contact directory (and not the local friend list) - if (!isFromRemoteDirectory && coreContext.contactsManager.isContactTemporary(friend, allowNullFriendList = true)) { - Log.i("$TAG Do not show friend [${friend.name}] which is in a temporary friend list") - continue - } - if (friend.refKey.orEmpty().isEmpty()) { if (friend.vcard != null) { friend.vcard?.generateUniqueId() From 26df085df331c72830796efe5a7f672d543abdbb Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 9 Apr 2025 10:19:56 +0200 Subject: [PATCH 095/593] Hide push notification setting in third party SIP accounts parameters, they won't work anyway + disable push for existing third party SIP accounts when migrating to 6.0.4 --- .../java/org/linphone/core/CoreContext.kt | 19 ++++++++++++++++- .../viewmodel/AccountSettingsViewModel.kt | 21 +++++++++++++------ .../res/layout/account_settings_fragment.xml | 2 ++ app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 5 files changed, 37 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index ca9d358a7b..1f2d790ff0 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -602,6 +602,8 @@ class CoreContext if (oldVersion < 600000) { // 6.0.0 initial release configurationMigration5To6() + } else if (oldVersion < 600004) { // 6.0.4 + disablePushNotificationsFromThirdPartySipAccounts() } if (core.logCollectionUploadServerUrl.isNullOrEmpty()) { @@ -1043,6 +1045,21 @@ class CoreContext logcatEnabled = enable } + // Migration between versions related + + @WorkerThread + private fun disablePushNotificationsFromThirdPartySipAccounts() { + for (account in core.accountList) { + val params = account.params + if (params.identityAddress?.domain != corePreferences.defaultDomain && params.pushNotificationAllowed) { + val clone = params.clone() + clone.pushNotificationAllowed = false + Log.w("$TAG Updating account [${params.identityAddress?.asStringUriOnly()}] params to disable push notifications, they won't work and may cause issues when used with UDP transport protocol") + account.params = clone + } + } + } + @WorkerThread private fun configurationMigration5To6() { val policy = core.videoActivationPolicy.clone() @@ -1069,7 +1086,7 @@ class CoreContext for (account in core.accountList) { val params = account.params - if (params.domain == corePreferences.defaultDomain && params.limeAlgo.isNullOrEmpty()) { + if (params.identityAddress?.domain == corePreferences.defaultDomain && params.limeAlgo.isNullOrEmpty()) { val clone = params.clone() clone.limeAlgo = "c25519" Log.i("$TAG Updating account [${params.identityAddress?.asStringUriOnly()}] params to use LIME algo c25519") diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt index 4ee95049f5..ce734ac7e5 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt @@ -24,6 +24,7 @@ import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import java.util.Locale import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.core.AVPFMode import org.linphone.core.Account import org.linphone.core.AuthInfo @@ -45,6 +46,8 @@ class AccountSettingsViewModel val expandNatPolicySettings = MutableLiveData() + val isOnDefaultDomain = MutableLiveData() + val pushNotificationsAvailable = MutableLiveData() val pushNotificationsEnabled = MutableLiveData() @@ -129,11 +132,16 @@ class AccountSettingsViewModel account = found val params = account.params - - pushNotificationsAvailable.postValue(core.isPushNotificationAvailable) - pushNotificationsEnabled.postValue( - core.isPushNotificationAvailable && params.pushNotificationAllowed - ) + val defaultDomain = params.identityAddress?.domain == corePreferences.defaultDomain + isOnDefaultDomain.postValue(defaultDomain) + if (defaultDomain) { + pushNotificationsAvailable.postValue(core.isPushNotificationAvailable) + pushNotificationsEnabled.postValue( + core.isPushNotificationAvailable && params.pushNotificationAllowed + ) + } else { + Log.w("$TAG Account isn't on default domain [${corePreferences.defaultDomain}], do not show push notification settings") + } imEncryptionMandatory.postValue(params.instantMessagingEncryptionMandatory) @@ -199,7 +207,8 @@ class AccountSettingsViewModel if (::account.isInitialized) { val newParams = account.params.clone() - newParams.pushNotificationAllowed = pushNotificationsEnabled.value == true + + newParams.pushNotificationAllowed = core.isPushNotificationAvailable && pushNotificationsEnabled.value == true newParams.instantMessagingEncryptionMandatory = imEncryptionMandatory.value == true diff --git a/app/src/main/res/layout/account_settings_fragment.xml b/app/src/main/res/layout/account_settings_fragment.xml index 4370b094d7..54de76c619 100644 --- a/app/src/main/res/layout/account_settings_fragment.xml +++ b/app/src/main/res/layout/account_settings_fragment.xml @@ -82,6 +82,7 @@ android:layout_marginEnd="16dp" android:enabled="@{viewModel.pushNotificationsAvailable}" android:checked="@={viewModel.pushNotificationsEnabled}" + android:visibility="@{viewModel.isOnDefaultDomain ? View.VISIBLE : View.GONE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"/> @@ -96,6 +97,7 @@ android:text="@{viewModel.pushNotificationsAvailable ? @string/account_settings_push_notification_title : @string/account_settings_push_notification_not_available_title, default=@string/account_settings_push_notification_title}" android:maxLines="2" android:ellipsize="end" + android:visibility="@{viewModel.isOnDefaultDomain ? View.VISIBLE : View.GONE}" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="@id/push_notifications_switch" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f58ba37ff1..2ea2f5ba5c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -118,7 +118,7 @@ Vous avez déjà un compte ? Transport Certaines fonctionnalités telles que les conversations de groupe, les vidéo-conférences, etc… nécessitent un compte &appName;.\n\nCes fonctionnalités seront masquées si vous utilisez un compte SIP tiers.\n\nPour les activer dans un projet commercial, merci de nous contacter. - Je préfère créer un compte + Je préfère créer un compte &appName; J\'ai compris Notifications push indisponible, la création de compte est donc désactivée. Notification push non reçue, merci de réessayer plus tard diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e7e453a109..ba060956af 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -158,7 +158,7 @@ Already have an account? Transport Some features require a &appName; account, such as group messaging, video conferences…\n\nThese features are hidden when you register with a third party SIP account.\n\nTo enable it in a commercial project, please contact us. - I prefer to create an account + I prefer to create a &appName; account I understand Push notifications not available, account creation disabled Push notification with auth token not received in 5 seconds, please try again later From bc9a6581b1529c3ae63b9d0567cb2f2f9dc92860 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 10 Apr 2025 09:45:03 +0200 Subject: [PATCH 096/593] Added logs to call transfer (blind & attended) --- .../ui/call/fragment/TransferCallFragment.kt | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt index a46075aef3..be9a14d2ee 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt @@ -130,15 +130,18 @@ class TransferCallFragment : GenericCallFragment() { binding.callsList.setHasFixedSize(true) binding.contactsAndSuggestionsList.setHasFixedSize(true) + binding.contactsAndSuggestionsList.layoutManager = LinearLayoutManager(requireContext()) + binding.callsList.layoutManager = LinearLayoutManager(requireContext()) + + val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), contactsAdapter) + binding.contactsAndSuggestionsList.addItemDecoration(headerItemDecoration) + callsAdapter.callClickedEvent.observe(viewLifecycleOwner) { it.consume { model -> showConfirmAttendedTransferDialog(model) } } - val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), contactsAdapter) - binding.contactsAndSuggestionsList.addItemDecoration(headerItemDecoration) - contactsAdapter.onClickedEvent.observe(viewLifecycleOwner) { it.consume { model -> showConfirmBlindTransferDialog(model) @@ -156,9 +159,6 @@ class TransferCallFragment : GenericCallFragment() { } } - binding.contactsAndSuggestionsList.layoutManager = LinearLayoutManager(requireContext()) - binding.callsList.layoutManager = LinearLayoutManager(requireContext()) - viewModel.modelsList.observe( viewLifecycleOwner ) { @@ -261,10 +261,13 @@ class TransferCallFragment : GenericCallFragment() { } private fun showConfirmAttendedTransferDialog(callModel: CallModel) { + val from = callViewModel.displayedName.value.orEmpty() + val to = callModel.displayName.value.orEmpty() + Log.i("$TAG Asking user confirmation before doing attended transfer of call with [$from] to [$to](${callModel.call.remoteAddress.asStringUriOnly()})") val label = AppUtils.getFormattedString( R.string.call_transfer_confirm_dialog_message, - callViewModel.displayedName.value.orEmpty(), - callModel.displayName.value.orEmpty() + from, + to ) val model = ConfirmationDialogModel(label) val dialog = DialogUtils.getConfirmCallTransferCallDialog( @@ -274,6 +277,7 @@ class TransferCallFragment : GenericCallFragment() { model.cancelEvent.observe(viewLifecycleOwner) { it.consume { + Log.i("$TAG Attended transfer was cancelled by user") dialog.dismiss() } } @@ -297,10 +301,13 @@ class TransferCallFragment : GenericCallFragment() { } private fun showConfirmBlindTransferDialog(contactModel: ConversationContactOrSuggestionModel) { + val from = callViewModel.displayedName.value.orEmpty() + val to = contactModel.name + Log.i("$TAG Asking user confirmation before doing blind transfer of call with [$from] to [$to](${contactModel.address.asStringUriOnly()})") val label = AppUtils.getFormattedString( R.string.call_transfer_confirm_dialog_message, - callViewModel.displayedName.value.orEmpty(), - contactModel.name + from, + to ) val model = ConfirmationDialogModel(label) val dialog = DialogUtils.getConfirmCallTransferCallDialog( @@ -310,6 +317,7 @@ class TransferCallFragment : GenericCallFragment() { model.cancelEvent.observe(viewLifecycleOwner) { it.consume { + Log.i("$TAG Blind transfer was cancelled by user") dialog.dismiss() } } From af3b1fa4186ce3623f7b5cf886d97db25b01c8c0 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 10 Apr 2025 10:04:26 +0200 Subject: [PATCH 097/593] Bumped dependencies --- gradle/libs.versions.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b7cd88a115..7eb414e425 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ agp = "8.9.1" kotlin = "2.0.21" gmsGoogleServices = "4.4.2" firebaseCrashlytics = "3.0.3" -firebaseBomVersion = "33.11.0" +firebaseBomVersion = "33.12.0" ktlint = "12.1.2" annotations = "1.9.1" @@ -12,13 +12,13 @@ appcompat = "1.7.0" constraintLayout = "2.2.1" coreKtx = "1.15.0" splashscreen = "1.2.0-beta01" -telecom = "1.0.0-beta01" +telecom = "1.0.0-rc01" media = "1.7.0" recyclerview = "1.4.0" slidingpanelayout = "1.2.0" window = "1.3.0" -gridlayout = "1.0.0" -securityCryptoKtx = "1.1.0-alpha06" +gridlayout = "1.1.0" +securityCryptoKtx = "1.1.0-alpha07" navigation = "2.8.9" emoji2 = "1.5.0" car = "1.7.0-rc01" From 51d725c757d1228e14996a386200cbfeb63e1c18 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 10 Apr 2025 13:24:10 +0200 Subject: [PATCH 098/593] Quick code cleanup --- .../org/linphone/contacts/ContactsManager.kt | 12 ++--------- .../notifications/NotificationsManager.kt | 20 ------------------- .../contacts/fragment/ContactsListFragment.kt | 1 - .../RecordingMediaPlayerViewModel.kt | 3 ++- .../layout/settings_developer_fragment.xml | 3 +-- 5 files changed, 5 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 5950197572..21130c6ed7 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -469,13 +469,13 @@ class ContactsManager Log.d( "$TAG Friend wasn't found using phone number [$username], looking in native address book directly" ) - findNativeContact(sipAddress, username, true) + null } } else { Log.d( "$TAG Friend wasn't found using SIP address [$sipAddress] and username [$username] isn't a phone number, looking in native address book directly" ) - findNativeContact(sipAddress, username.orEmpty(), false) + null } } @@ -644,14 +644,6 @@ class ContactsManager return temporaryFriendList } - @WorkerThread - fun findNativeContact(address: String, username: String, searchAsPhoneNumber: Boolean): Friend? { - // As long as read contacts permission is granted, friends will be stored in DB, - // so if Core didn't find a matching item it in the FriendList, there's no reason the native address book - // shall contain a matching contact. - return null - } - @WorkerThread fun getMePerson(localAddress: Address): Person { val account = coreContext.core.accountList.find { diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 7f60c76257..b8624bddc2 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -31,9 +31,7 @@ import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap import android.media.AudioAttributes -import android.media.AudioManager import android.media.MediaPlayer -import android.media.RingtoneManager import android.net.Uri import android.os.Bundle import androidx.annotation.AnyThread @@ -1592,24 +1590,6 @@ class NotificationsManager currentKeepAliveThirdPartyAccountsForegroundServiceNotificationId = -1 } - @MainThread - private fun createIncomingCallNotificationChannel() { - val id = context.getString(R.string.notification_channel_incoming_call_id) - val name = context.getString(R.string.notification_channel_incoming_call_name) - - val ringtone = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) - val audioAttributes = AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) - .setLegacyStreamType(AudioManager.STREAM_RING) - .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE).build() - - val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH).apply { - description = name - setSound(ringtone, audioAttributes) - } - notificationManager.createNotificationChannel(channel) - } - @MainThread private fun createIncomingCallNotificationChannelWithoutRingtone() { val id = context.getString(R.string.notification_channel_without_ringtone_incoming_call_id) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt index 8a372b1bb9..cdf98eb857 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt @@ -37,7 +37,6 @@ import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.observe import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager diff --git a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt index b025be7698..2440e7de5b 100644 --- a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt @@ -98,9 +98,10 @@ class RecordingMediaPlayerViewModel @UiThread fun setVideoRenderingSurface(textureView: TextureView) { + val texture = textureView.surfaceTexture coreContext.postOnCoreThread { Log.i("$TAG Setting window ID in player") - player.setWindowId(textureView.surfaceTexture) + player.setWindowId(texture) } } diff --git a/app/src/main/res/layout/settings_developer_fragment.xml b/app/src/main/res/layout/settings_developer_fragment.xml index 3c7fc9012e..aa6267de0b 100644 --- a/app/src/main/res/layout/settings_developer_fragment.xml +++ b/app/src/main/res/layout/settings_developer_fragment.xml @@ -1,7 +1,6 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> From e2dfd95857a1e13edec8e775ab6604a4a7c337d1 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 10 Apr 2025 14:12:39 +0200 Subject: [PATCH 099/593] Prevent crash in HelpViewModel if app is built without Firebase --- .../org/linphone/ui/main/help/viewmodel/HelpViewModel.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/main/help/viewmodel/HelpViewModel.kt b/app/src/main/java/org/linphone/ui/main/help/viewmodel/HelpViewModel.kt index 32dab37fe5..3b33ae420b 100644 --- a/app/src/main/java/org/linphone/ui/main/help/viewmodel/HelpViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/help/viewmodel/HelpViewModel.kt @@ -149,7 +149,12 @@ class HelpViewModel sdkVersion.value = coreContext.sdkVersion logsUploadInProgress.value = false - firebaseProjectId.value = FirebaseApp.getInstance().options.projectId + try { + firebaseProjectId.value = FirebaseApp.getInstance().options.projectId + } catch (e: Exception) { + Log.e("$TAG Failed to get FirebaseApp instance: $e") + firebaseProjectId.value = "unknown" + } coreContext.postOnCoreThread { core -> core.addListener(coreListener) From 518ecc182332e41df5233f88aace4b5be987f643 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 11 Apr 2025 09:37:54 +0200 Subject: [PATCH 100/593] Added a list of domain for which to show push notification settings --- app/src/main/java/org/linphone/core/CoreContext.kt | 3 ++- .../main/java/org/linphone/core/CorePreferences.kt | 13 +++++++++---- .../settings/viewmodel/AccountSettingsViewModel.kt | 8 ++++---- .../main/res/layout/account_settings_fragment.xml | 4 ++-- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 1f2d790ff0..d64f615a84 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -1051,7 +1051,8 @@ class CoreContext private fun disablePushNotificationsFromThirdPartySipAccounts() { for (account in core.accountList) { val params = account.params - if (params.identityAddress?.domain != corePreferences.defaultDomain && params.pushNotificationAllowed) { + val pushAvailableForDomain = params.identityAddress?.domain in corePreferences.pushNotificationCompatibleDomains + if (!pushAvailableForDomain && params.pushNotificationAllowed) { val clone = params.clone() clone.pushNotificationAllowed = false Log.w("$TAG Updating account [${params.identityAddress?.asStringUriOnly()}] params to disable push notifications, they won't work and may cause issues when used with UDP transport protocol") diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 99b9da0512..76aa5d0d90 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -276,6 +276,15 @@ class CorePreferences config.setString("ui", "theme_main_color", value) } + // Customization options + + @get:WorkerThread + val defaultDomain: String + get() = config.getString("app", "default_domain", "sip.linphone.org")!! + + val pushNotificationCompatibleDomains: Array + get() = config.getStringList("app", "push_notification_domains", arrayOf("sip.linphone.org")) + @get:WorkerThread val darkModeAllowed: Boolean get() = config.getBool("ui", "dark_mode_allowed", true) @@ -378,10 +387,6 @@ class CorePreferences // Paths - @get:WorkerThread - val defaultDomain: String - get() = config.getString("app", "default_domain", "sip.linphone.org")!! - @get:AnyThread val configPath: String get() = context.filesDir.absolutePath + "/" + CONFIG_FILE_NAME diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt index ce734ac7e5..ed201c7111 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt @@ -46,7 +46,7 @@ class AccountSettingsViewModel val expandNatPolicySettings = MutableLiveData() - val isOnDefaultDomain = MutableLiveData() + val isDomainInPushNotificationCompatibleList = MutableLiveData() val pushNotificationsAvailable = MutableLiveData() @@ -132,9 +132,9 @@ class AccountSettingsViewModel account = found val params = account.params - val defaultDomain = params.identityAddress?.domain == corePreferences.defaultDomain - isOnDefaultDomain.postValue(defaultDomain) - if (defaultDomain) { + val pushAvailableForDomain = params.identityAddress?.domain in corePreferences.pushNotificationCompatibleDomains + isDomainInPushNotificationCompatibleList.postValue(pushAvailableForDomain) + if (pushAvailableForDomain) { pushNotificationsAvailable.postValue(core.isPushNotificationAvailable) pushNotificationsEnabled.postValue( core.isPushNotificationAvailable && params.pushNotificationAllowed diff --git a/app/src/main/res/layout/account_settings_fragment.xml b/app/src/main/res/layout/account_settings_fragment.xml index 54de76c619..759350c243 100644 --- a/app/src/main/res/layout/account_settings_fragment.xml +++ b/app/src/main/res/layout/account_settings_fragment.xml @@ -82,7 +82,7 @@ android:layout_marginEnd="16dp" android:enabled="@{viewModel.pushNotificationsAvailable}" android:checked="@={viewModel.pushNotificationsEnabled}" - android:visibility="@{viewModel.isOnDefaultDomain ? View.VISIBLE : View.GONE}" + android:visibility="@{viewModel.isDomainInPushNotificationCompatibleList ? View.VISIBLE : View.GONE}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"/> @@ -97,7 +97,7 @@ android:text="@{viewModel.pushNotificationsAvailable ? @string/account_settings_push_notification_title : @string/account_settings_push_notification_not_available_title, default=@string/account_settings_push_notification_title}" android:maxLines="2" android:ellipsize="end" - android:visibility="@{viewModel.isOnDefaultDomain ? View.VISIBLE : View.GONE}" + android:visibility="@{viewModel.isDomainInPushNotificationCompatibleList ? View.VISIBLE : View.GONE}" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="@id/push_notifications_switch" app:layout_constraintStart_toStartOf="parent" From b1b1ab0d8aba5a112c3d22159af8f7ccf0f86a75 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 11 Apr 2025 10:28:59 +0200 Subject: [PATCH 101/593] Updated CHANGELOG & version code from release/6.0 branch --- CHANGELOG.md | 9 +++++++++ app/build.gradle.kts | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4acc2b8fb1..a0c3a0eb5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,15 @@ Group changes to describe their impact on the project, as follows: Fixed for any bug fixes. Security to invite users to upgrade in case of vulnerabilities. +## [6.0.4] - 2025-04-11 + +### Changed +- Third party SIP accounts push notifications will be disabled and setting will be hidden unless if list of supported domains (to prevent issues, specifically when used with UDP transport protocol causing bigger packets getting lost) + +### Fixed +- Prevent refresh of views due to contacts changes to happen to frequently at startup +- Prevent crash in Help view if app is built without Firebase + ## [6.0.3] - 2025-04-04 ### Added diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 970897087d..01d0b20a4f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,7 +32,7 @@ if (firebaseCloudMessagingAvailable) { } var gitBranch = ByteArrayOutputStream() -var gitVersion = "6.0.3" +var gitVersion = "6.0.4" task("getGitVersion") { val gitVersionStream = ByteArrayOutputStream() @@ -100,8 +100,8 @@ android { applicationId = packageName minSdk = 28 targetSdk = 36 - versionCode = 600003 // 6.00.003 - versionName = "6.0.3" + versionCode = 600004 // 6.00.004 + versionName = "6.0.4" manifestPlaceholders["appAuthRedirectScheme"] = packageName From 6ba8760be7c1b2e0716481b4064323e381b5897a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 15 Apr 2025 11:41:11 +0200 Subject: [PATCH 102/593] Improved called account display --- .../ui/call/viewmodel/CurrentCallViewModel.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 502a1d28a3..3e46cc2bea 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -1115,7 +1115,19 @@ class CurrentCallViewModel if (call.dir == Call.Dir.Incoming) { val isVideo = call.remoteParams?.isVideoEnabled == true && call.remoteParams?.videoDirection != MediaDirection.Inactive if (call.core.accountList.size > 1) { - val displayName = LinphoneUtils.getDisplayName(call.toAddress) + val localAddress = call.callLog.toAddress + Log.i("$TAG Local address for incoming call is [${localAddress.asStringUriOnly()}]") + val localAccount = coreContext.core.accountList.find { + it.params.identityAddress?.weakEqual(localAddress) == true + } + val displayName = if (localAccount != null) { + LinphoneUtils.getDisplayName(localAccount.params.identityAddress) + } else { + Log.w("$TAG Matching local account was not found, using TO address display name or username") + LinphoneUtils.getDisplayName(localAddress) + } + Log.i("$TAG Showing account being called as [$displayName]") + if (isVideo) { incomingCallTitle.postValue( AppUtils.getFormattedString( From 94b6db6a088588181089f89273d3acebb48ea0ac Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 17 Apr 2025 09:40:52 +0200 Subject: [PATCH 103/593] Fixed build with latest SDK --- .../linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt b/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt index a0fe73c776..80e468920f 100644 --- a/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt @@ -345,9 +345,9 @@ class SingleSignOnViewModel onErrorEvent.postValue(Event("Invalid access token expiration time")) } else { val accessToken = - Factory.instance().createBearerToken(authState.accessToken, expire / 1000) // Linphone timestamps are in seconds + Factory.instance().createBearerToken(authState.accessToken.orEmpty(), expire / 1000) // Linphone timestamps are in seconds val refreshToken = - Factory.instance().createBearerToken(authState.refreshToken, expire / 1000) // Linphone timestamps are in seconds + Factory.instance().createBearerToken(authState.refreshToken.orEmpty(), expire / 1000) // Linphone timestamps are in seconds val authInfo = coreContext.bearerAuthInfoPendingPasswordUpdate if (authInfo == null) { From afa041baf6a661024a4ef45148c795ecf088ff51 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 16 Apr 2025 10:59:20 +0200 Subject: [PATCH 104/593] Hide account creation form when device doesn't support push notifications --- .../ui/assistant/fragment/RegisterFragment.kt | 10 --- .../assistant_register_fragment.xml | 72 +++++++++++++++++-- ...ird_party_sip_account_warning_fragment.xml | 2 +- .../layout/assistant_register_fragment.xml | 58 +++++++++++++-- ...ird_party_sip_account_warning_fragment.xml | 4 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 7 files changed, 125 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt index 8cfee0ff28..1afb07dea1 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt @@ -140,16 +140,6 @@ class RegisterFragment : GenericFragment() { override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} }) - viewModel.pushNotificationsAvailable.observe(viewLifecycleOwner) { available -> - if (!available) { - val text = getString(R.string.assistant_account_register_unavailable_no_push_toast) - (requireActivity() as GenericActivity).showRedToast( - text, - R.drawable.warning_circle - ) - } - } - viewModel.normalizedPhoneNumberEvent.observe(viewLifecycleOwner) { it.consume { number -> showPhoneNumberConfirmationDialog(number) diff --git a/app/src/main/res/layout-sw600dp/assistant_register_fragment.xml b/app/src/main/res/layout-sw600dp/assistant_register_fragment.xml index 75fc8a3b84..9831e522c0 100644 --- a/app/src/main/res/layout-sw600dp/assistant_register_fragment.xml +++ b/app/src/main/res/layout-sw600dp/assistant_register_fragment.xml @@ -37,6 +37,18 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> + + + + + + + + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/subscribe_barrier" /> Certaines fonctionnalités telles que les conversations de groupe, les vidéo-conférences, etc… nécessitent un compte &appName;.\n\nCes fonctionnalités seront masquées si vous utilisez un compte SIP tiers.\n\nPour les activer dans un projet commercial, merci de nous contacter. Je préfère créer un compte &appName; J\'ai compris - Notifications push indisponible, la création de compte est donc désactivée. + Les notifications push ne semblent pas être disponibles sur votre appareil. Celles-ci sont nécessaires à la création d’un compte sur l’application mobile.\n\nNous vous invitons à créer un compte depuis notre plateforme web : Notification push non reçue, merci de réessayer plus tard Un erreur inattendue est survenue, merci de réessayer plus tard Mauvais nom d\'utilisateur ou mot de passe diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ba060956af..9579b9ee2d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -160,7 +160,7 @@ Some features require a &appName; account, such as group messaging, video conferences…\n\nThese features are hidden when you register with a third party SIP account.\n\nTo enable it in a commercial project, please contact us. I prefer to create a &appName; account I understand - Push notifications not available, account creation disabled + Push notifications do not seem to be available on your device, but they are mandatory for creating an account in the mobile app.\n\nWe invite you to create an account on our web platform instead: Push notification with auth token not received in 5 seconds, please try again later Unexpected error occurred, please try again later Wrong username or password From c64bd5bc1cbe0bbf42727017c831b9fbac917bc5 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 16 Apr 2025 18:46:20 +0200 Subject: [PATCH 105/593] Fixed numpad dial button while transfering a call --- .../ui/call/fragment/TransferCallFragment.kt | 20 +++++++---- .../ui/call/viewmodel/CurrentCallViewModel.kt | 2 ++ .../ui/main/history/model/NumpadModel.kt | 7 ++++ .../history/viewmodel/StartCallViewModel.kt | 22 ++++++++++++ .../res/layout/call_transfer_fragment.xml | 1 + .../layout/start_call_numpad_bottom_sheet.xml | 36 +++++++++++++++++-- 6 files changed, 78 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt index be9a14d2ee..b2b58745c4 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt @@ -35,6 +35,7 @@ import kotlin.getValue import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R +import org.linphone.core.Address import org.linphone.core.tools.Log import org.linphone.databinding.CallTransferFragmentBinding import org.linphone.ui.call.adapter.CallsListAdapter @@ -44,9 +45,9 @@ import org.linphone.ui.call.viewmodel.CurrentCallViewModel import org.linphone.ui.main.adapter.ConversationsContactsAndSuggestionsListAdapter import org.linphone.ui.main.history.viewmodel.StartCallViewModel import org.linphone.utils.ConfirmationDialogModel -import org.linphone.ui.main.model.ConversationContactOrSuggestionModel import org.linphone.utils.AppUtils import org.linphone.utils.DialogUtils +import org.linphone.utils.LinphoneUtils import org.linphone.utils.RecyclerViewHeaderDecoration import org.linphone.utils.hideKeyboard import org.linphone.utils.setKeyboardInsetListener @@ -144,7 +145,7 @@ class TransferCallFragment : GenericCallFragment() { contactsAdapter.onClickedEvent.observe(viewLifecycleOwner) { it.consume { model -> - showConfirmBlindTransferDialog(model) + showConfirmBlindTransferDialog(model.address, model.name) } } @@ -231,6 +232,12 @@ class TransferCallFragment : GenericCallFragment() { } } + viewModel.initiateBlindTransferEvent.observe(viewLifecycleOwner) { + it.consume { address -> + showConfirmBlindTransferDialog(address, LinphoneUtils.getDisplayName(address)) + } + } + binding.root.setKeyboardInsetListener { keyboardVisible -> if (keyboardVisible) { viewModel.isNumpadVisible.value = false @@ -300,14 +307,13 @@ class TransferCallFragment : GenericCallFragment() { dialog.show() } - private fun showConfirmBlindTransferDialog(contactModel: ConversationContactOrSuggestionModel) { + private fun showConfirmBlindTransferDialog(toAddress: Address, toDisplayName: String) { val from = callViewModel.displayedName.value.orEmpty() - val to = contactModel.name - Log.i("$TAG Asking user confirmation before doing blind transfer of call with [$from] to [$to](${contactModel.address.asStringUriOnly()})") + Log.i("$TAG Asking user confirmation before doing blind transfer of call with [$from] to [$toDisplayName](${toAddress.asStringUriOnly()})") val label = AppUtils.getFormattedString( R.string.call_transfer_confirm_dialog_message, from, - to + toDisplayName ) val model = ConfirmationDialogModel(label) val dialog = DialogUtils.getConfirmCallTransferCallDialog( @@ -325,7 +331,7 @@ class TransferCallFragment : GenericCallFragment() { model.confirmEvent.observe(viewLifecycleOwner) { it.consume { coreContext.postOnCoreThread { - val address = contactModel.address + val address = toAddress Log.i("$TAG Transferring (blind) call to [${address.asStringUriOnly()}]") callViewModel.blindTransferCallTo(address) } diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 3e46cc2bea..891c3e75ef 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -580,6 +580,8 @@ class CurrentCallViewModel }, { // OnCallClicked }, + { // OnBlindTransferClicked + }, { // OnClearInput } ) diff --git a/app/src/main/java/org/linphone/ui/main/history/model/NumpadModel.kt b/app/src/main/java/org/linphone/ui/main/history/model/NumpadModel.kt index 2a98dafb76..9a74695c99 100644 --- a/app/src/main/java/org/linphone/ui/main/history/model/NumpadModel.kt +++ b/app/src/main/java/org/linphone/ui/main/history/model/NumpadModel.kt @@ -34,6 +34,7 @@ open class NumpadModel private val onVoicemailClicked: () -> (Unit), private val onBackspaceClicked: () -> (Unit), private val onCallClicked: () -> (Unit), + private val onTransferCallClicked: () -> (Unit), private val onClearClicked: () -> (Unit) ) { companion object { @@ -106,4 +107,10 @@ open class NumpadModel Log.i("$TAG Starting call") onCallClicked.invoke() } + + @UiThread + fun onBlindTransferClicked() { + Log.i("$TAG Transferring call") + onTransferCallClicked.invoke() + } } diff --git a/app/src/main/java/org/linphone/ui/main/history/viewmodel/StartCallViewModel.kt b/app/src/main/java/org/linphone/ui/main/history/viewmodel/StartCallViewModel.kt index b19f385ea6..cb30544285 100644 --- a/app/src/main/java/org/linphone/ui/main/history/viewmodel/StartCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/history/viewmodel/StartCallViewModel.kt @@ -80,6 +80,10 @@ class StartCallViewModel MutableLiveData>() } + val initiateBlindTransferEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + private val conferenceListener = object : ConferenceListenerStub() { @WorkerThread override fun onStateChanged(conference: Conference, newState: Conference.State?) { @@ -139,6 +143,24 @@ class StartCallViewModel } } }, + { // OnBlindTransferClicked + val suggestion = searchFilter.value.orEmpty() + if (suggestion.isNotEmpty()) { + Log.i("$TAG Using numpad transfer button to blind forward call to [$suggestion]") + coreContext.postOnCoreThread { core -> + val address = core.interpretUrl( + suggestion, + LinphoneUtils.applyInternationalPrefix() + ) + if (address != null) { + initiateBlindTransferEvent.postValue(Event(address)) + leaveFragmentEvent.postValue(Event(true)) + } else { + Log.e("$TAG Failed to parse [$suggestion] as SIP address") + } + } + } + }, { // OnClearInput clearSearchBarEvent.value = Event(true) } diff --git a/app/src/main/res/layout/call_transfer_fragment.xml b/app/src/main/res/layout/call_transfer_fragment.xml index d63a24017d..4b53929508 100644 --- a/app/src/main/res/layout/call_transfer_fragment.xml +++ b/app/src/main/res/layout/call_transfer_fragment.xml @@ -220,6 +220,7 @@ android:id="@+id/numpad_layout" bind:handleClickedListener="@{hideNumpadClickListener}" bind:model="@{viewModel.numpadModel}" + bind:showCallTransferIcon="@{true}" layout="@layout/start_call_numpad_bottom_sheet" /> diff --git a/app/src/main/res/layout/start_call_numpad_bottom_sheet.xml b/app/src/main/res/layout/start_call_numpad_bottom_sheet.xml index ee886f1d9a..dc7b51ec74 100644 --- a/app/src/main/res/layout/start_call_numpad_bottom_sheet.xml +++ b/app/src/main/res/layout/start_call_numpad_bottom_sheet.xml @@ -11,6 +11,9 @@ + + + + + From 5ef7eab0c53b409803f0a5f4b49dcc4bca068d8a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 17 Apr 2025 13:07:15 +0200 Subject: [PATCH 106/593] Added microphone volume vu meter --- .../org/linphone/ui/call/view/VuMeterView.kt | 99 +++++++++++++++++++ .../ui/call/viewmodel/CurrentCallViewModel.kt | 28 ++++++ .../org/linphone/utils/DataBindingUtils.kt | 7 ++ .../main/res/layout/call_actions_generic.xml | 24 ++++- .../main/res/layout/call_outgoing_actions.xml | 27 ++++- app/src/main/res/values/colors.xml | 1 + 6 files changed, 181 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/org/linphone/ui/call/view/VuMeterView.kt diff --git a/app/src/main/java/org/linphone/ui/call/view/VuMeterView.kt b/app/src/main/java/org/linphone/ui/call/view/VuMeterView.kt new file mode 100644 index 0000000000..6aa39bf1f1 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/call/view/VuMeterView.kt @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2010-2025 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.ui.call.view + +import android.content.Context +import android.graphics.BitmapShader +import android.graphics.Canvas +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Shader +import android.util.AttributeSet +import android.view.View +import androidx.core.content.ContextCompat +import androidx.core.graphics.createBitmap +import org.linphone.R + +class VuMeterView : View { + companion object { + private const val TAG = "[VuMeter View]" + } + + private lateinit var paint: Paint + private lateinit var matrix: Matrix + private lateinit var vuMeterPaint: Paint + + private val color = ContextCompat.getColor(context, R.color.vu_meter) + + private var vuMeterPercentage: Float = 0f + + constructor(context: Context?) : super(context) { + init() + } + + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { + init() + } + + constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super( + context, + attrs, + defStyle + ) { + init() + } + + private fun init() { + paint = Paint() + paint.isAntiAlias = true + matrix = Matrix() + + vuMeterPaint = Paint() + vuMeterPaint.strokeWidth = 2f + vuMeterPaint.isAntiAlias = true + vuMeterPaint.setColor(color) + } + + fun setVuMeterPercentage(percentage: Float) { + vuMeterPercentage = percentage + invalidate() + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + createShader() + } + + private fun createShader(): Shader { + val level = (height - height * vuMeterPercentage).toFloat() + + val bitmap = createBitmap(width, height) + val canvas = Canvas(bitmap) + canvas.drawRect(0f, height.toFloat(), width.toFloat(), level, vuMeterPaint) + + val shader = BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP) + return shader + } + + override fun onDraw(canvas: Canvas) { + paint.setShader(createShader()) + canvas.drawCircle(width / 2f, height / 2f, width / 2f, paint) + } +} diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 891c3e75ef..c76b437716 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -29,6 +29,9 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.linphone.LinphoneApplication.Companion.coreContext @@ -69,6 +72,8 @@ class CurrentCallViewModel constructor() : GenericViewModel() { companion object { private const val TAG = "[Current Call ViewModel]" + private const val VU_METER_MIN = -20f + private const val VU_METER_MAX = 4 } val contact = MutableLiveData() @@ -107,6 +112,8 @@ class CurrentCallViewModel val isMicrophoneMuted = MutableLiveData() + val microphoneRecordingVolume = MutableLiveData() + val isSpeakerEnabled = MutableLiveData() val isHeadsetEnabled = MutableLiveData() @@ -541,6 +548,7 @@ class CurrentCallViewModel operationInProgress.value = false proximitySensorEnabled.value = false videoUpdateInProgress.value = false + microphoneRecordingVolume.value = 0f coreContext.postOnCoreThread { core -> hideSipAddresses.postValue(corePreferences.hideSipAddresses) @@ -1247,6 +1255,13 @@ class CurrentCallViewModel } else { Log.i("$TAG Failed to find an existing 1-1 conversation for current call") } + + microphoneVolumeVuMeterTickerFlow().onEach { + coreContext.postOnCoreThread { + val volumeDbm0 = currentCall.recordVolume + microphoneRecordingVolume.postValue(computeVuMeterValue(volumeDbm0)) + } + }.launchIn(viewModelScope) } @WorkerThread @@ -1519,4 +1534,17 @@ class CurrentCallViewModel private fun showRecordingToast() { showGreenToast(R.string.call_is_being_recorded, R.drawable.record_fill) } + + private fun microphoneVolumeVuMeterTickerFlow() = flow { + while (::currentCall.isInitialized) { + emit(Unit) + delay(50) + } + } + + private fun computeVuMeterValue(volume: Float): Float { + if (volume < VU_METER_MIN) return 0f + if (volume > VU_METER_MAX) return 1f + return (volume - VU_METER_MIN) / (VU_METER_MAX - VU_METER_MIN) + } } diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index fff7527509..aaa67004e8 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -69,6 +69,7 @@ import org.linphone.core.ConsolidatedPresence import org.linphone.core.tools.Log import org.linphone.ui.NotoSansFont import org.linphone.ui.call.conference.model.ConferenceParticipantDeviceModel +import org.linphone.ui.call.view.VuMeterView import org.linphone.ui.call.view.RoundCornersTextureView /** @@ -501,6 +502,12 @@ fun setParticipantTextureView( model.setTextureView(textureView) } +@UiThread +@BindingAdapter("vuMeterPercentage") +fun setVuMeterPercentage(view: VuMeterView, percentage: Float) { + view.setVuMeterPercentage(percentage) +} + @UiThread @BindingAdapter("onValueChanged") fun AppCompatEditText.editTextSetting(lambda: () -> Unit) { diff --git a/app/src/main/res/layout/call_actions_generic.xml b/app/src/main/res/layout/call_actions_generic.xml index 3b854ad900..394b249c0b 100644 --- a/app/src/main/res/layout/call_actions_generic.xml +++ b/app/src/main/res/layout/call_actions_generic.xml @@ -77,6 +77,28 @@ app:layout_constraintStart_toStartOf="@id/toggle_video" app:layout_constraintEnd_toEndOf="@id/toggle_video"/> + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 10aaf2d1b1..7b931aeaa6 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -5,6 +5,7 @@ #FFFFFF #99000000 #10000000 + #3CFFFFFF #191919 #303030 From dcbc8371061d198b324c27f7705e685bc8f5af63 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 17 Apr 2025 14:30:23 +0200 Subject: [PATCH 107/593] Apply workaround when making a call to a SIP URI having a phone number as username & IP as domain --- .../compatibility/Api29Compatibility.kt | 5 ++++ .../linphone/compatibility/Compatibility.kt | 8 +++++ .../java/org/linphone/core/CoreContext.kt | 29 +++++++++++++++---- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt index 200e8c9f6b..6f0826d778 100644 --- a/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt @@ -20,6 +20,7 @@ package org.linphone.compatibility import android.content.Intent +import android.net.InetAddresses.isNumericAddress import android.net.Uri import android.os.Build import android.provider.MediaStore @@ -62,5 +63,9 @@ class Api29Compatibility { session.contentCaptureContext = ContentCaptureContext.forLocusId(conversationId) } } + + fun isIpAddress(string: String): Boolean { + return isNumericAddress(string) + } } } diff --git a/app/src/main/java/org/linphone/compatibility/Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Compatibility.kt index 9d828fb550..153dcf00ff 100644 --- a/app/src/main/java/org/linphone/compatibility/Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Compatibility.kt @@ -28,6 +28,7 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Environment +import android.util.Patterns import android.view.View import androidx.appcompat.app.AppCompatDelegate import org.linphone.core.tools.Log @@ -186,5 +187,12 @@ class Compatibility { Api35Compatibility.setupAppStartupListener(context) } } + + fun isIpAddress(string: String): Boolean { + if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { + return Api29Compatibility.isIpAddress(string) + } + return Patterns.IP_ADDRESS.matcher(string).matches() + } } } diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index d64f615a84..04a9f91303 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -34,12 +34,14 @@ import android.provider.Settings.SettingNotFoundException import androidx.annotation.AnyThread import androidx.annotation.UiThread import androidx.annotation.WorkerThread +import androidx.core.text.isDigitsOnly import androidx.lifecycle.MutableLiveData import com.google.firebase.crashlytics.FirebaseCrashlytics import kotlin.system.exitProcess import org.linphone.BuildConfig import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.compatibility.Compatibility import org.linphone.contacts.ContactsManager import org.linphone.core.tools.Log import org.linphone.notifications.NotificationsManager @@ -837,10 +839,6 @@ class CoreContext if (forceZRTP) { params.mediaEncryption = MediaEncryption.ZRTP } - /*if (LinphoneUtils.checkIfNetworkHasLowBandwidth(context)) { - Log.w("$TAG Enabling low bandwidth mode!") - params.isLowBandwidthEnabled = true - }*/ params.recordFile = LinphoneUtils.getRecordingFilePathForAddress(address) @@ -860,8 +858,27 @@ class CoreContext } } - val call = core.inviteAddressWithParams(address, params) - Log.i("$TAG Starting call $call") + val username = address.username.orEmpty() + val domain = address.domain.orEmpty() + val defaultAccount = params.account ?: core.defaultAccount + if (defaultAccount != null && Compatibility.isIpAddress(domain)) { + Log.i("$TAG SIP URI [${address.asStringUriOnly()}] seems to have an IP address as domain") + if (username.isNotEmpty() && (username.startsWith("+") || username.isDigitsOnly())) { + val identityDomain = defaultAccount.params.identityAddress?.domain + Log.w("$TAG Username [$username] looks like a phone number, replacing domain [$domain] by the local account one [$identityDomain]") + if (identityDomain != null) { + val newAddress = address.clone() + newAddress.domain = identityDomain + + core.inviteAddressWithParams(newAddress, params) + Log.i("$TAG Starting call to [${newAddress.asStringUriOnly()}]") + return + } + } + } + + core.inviteAddressWithParams(address, params) + Log.i("$TAG Starting call to [${address.asStringUriOnly()}]") } @WorkerThread From cd35f213c181c7c831cacfcb11a881f59c131a0a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 17 Apr 2025 17:19:29 +0200 Subject: [PATCH 108/593] Fixed crash due to missing foreground service if OS denies call notification until foreground Service was started + fixed crash if call is ended before CoreInCallService was started and foreground service notification sent --- .../org/linphone/core/CoreInCallService.kt | 4 + .../notifications/NotificationsManager.kt | 83 ++++++++++++------- 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreInCallService.kt b/app/src/main/java/org/linphone/core/CoreInCallService.kt index 0c26babd84..12dfe51ee1 100644 --- a/app/src/main/java/org/linphone/core/CoreInCallService.kt +++ b/app/src/main/java/org/linphone/core/CoreInCallService.kt @@ -61,6 +61,10 @@ class CoreInCallService : CoreService() { return null } + override fun createServiceNotificationChannel() { + // Do nothing, app's Notifications Manager will do the job + } + override fun createServiceNotification() { // Do nothing, app's Notifications Manager will do the job } diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index b8624bddc2..8b3809e3b7 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -237,13 +237,21 @@ class NotificationsManager Log.i( "$TAG Incoming call has been declined, cancelling incoming call notification" ) + if (waitForInCallServiceForegroundToStopIt) { + Log.w("$TAG We are waiting for service to be started as foreground, starting it now") + showCallNotification(call, false) + } removeIncomingCallNotification() + } else { + Log.i( + "$TAG Removing terminated/declined call notification for [${remoteSipAddress.asStringUriOnly()}]" + ) + if (waitForInCallServiceForegroundToStopIt) { + Log.w("$TAG We are waiting for service to be started as foreground, starting it now") + showCallNotification(call, false) + } + dismissCallNotification(call) } - - Log.i( - "$TAG Removing terminated/declined call notification for [${remoteSipAddress.asStringUriOnly()}]" - ) - dismissCallNotification(call) } Call.State.Released -> { if (LinphoneUtils.isCallLogMissed(call.callLog)) { @@ -257,13 +265,10 @@ class NotificationsManager @WorkerThread override fun onLastCallEnded(core: Core) { - Log.i("$TAG Last call ended, stopping foreground service") + Log.i("$TAG Last call ended") if (inCallServiceForegroundNotificationPublished) { - stopInCallCallForegroundService() - } else { - // Wait for foreground service to have been started before stopping it - Log.w("$TAG We would like to stop the foreground service but it wasn't started yet, wait for it") - waitForInCallServiceForegroundToStopIt = true + Log.i("$TAG Stopping foreground service") + stopInCallForegroundService() } } @@ -495,18 +500,16 @@ class NotificationsManager if (core.callsNb == 0) { Log.w("$TAG No call anymore, stopping service") if (inCallServiceForegroundNotificationPublished) { - stopInCallCallForegroundService() - } else { - // Wait for foreground service to have been started before stopping it - Log.w("$TAG We would like to stop the foreground service but it wasn't started yet, wait for it") - waitForInCallServiceForegroundToStopIt = true + stopInCallForegroundService() } } else if (currentInCallServiceNotificationId == -1) { + val call = core.currentCall ?: core.calls.first() Log.i( - "$TAG At least a call is still running and no foreground Service notification was found" + "$TAG At least one call is running and no foreground Service notification was found, starting it using call [${call.remoteAddress.asStringUriOnly()}]" ) - val call = core.currentCall ?: core.calls.first() - startInCallForegroundService(call) + + Log.i("$TAG No notification found for this call, creating one now") + showCallNotification(call, LinphoneUtils.isCallIncoming(call.state)) } } } @@ -649,14 +652,18 @@ class NotificationsManager ) if (isIncoming) { currentlyRingingCallRemoteAddress = call.remoteAddress - notify(INCOMING_CALL_ID, notification) if (currentInCallServiceNotificationId == -1) { - startIncomingCallForegroundService(notification) + Log.i("$TAG No current in-call foreground service notification found, using this one") + showIncomingCallForegroundServiceNotification(notification) + } else { + notify(INCOMING_CALL_ID, notification) } } else { - notify(notifiable.notificationId, notification) if (currentInCallServiceNotificationId == -1) { - startInCallForegroundService(call) + Log.i("$TAG No current in-call foreground service notification found, using this one") + showInCallForegroundServiceNotification(call, notifiable, notification) + } else { + notify(notifiable.notificationId, notification) } } } @@ -709,7 +716,7 @@ class NotificationsManager } @WorkerThread - private fun startIncomingCallForegroundService(notification: Notification) { + private fun showIncomingCallForegroundServiceNotification(notification: Notification) { Log.i("$TAG Trying to start foreground Service using incoming call notification") val service = inCallService if (service != null) { @@ -723,11 +730,12 @@ class NotificationsManager notification, Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL ) + notificationsMap[INCOMING_CALL_ID] = notification currentInCallServiceNotificationId = INCOMING_CALL_ID inCallServiceForegroundNotificationPublished = true if (waitForInCallServiceForegroundToStopIt) { Log.i("$TAG We were waiting for foreground service to be started to stop it, doing it") - stopInCallCallForegroundService() + stopInCallForegroundService() } } else { Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground service!") @@ -742,7 +750,7 @@ class NotificationsManager if (LinphoneUtils.isCallIncoming(call.state)) { val notification = notificationsMap[INCOMING_CALL_ID] if (notification != null) { - startIncomingCallForegroundService(notification) + showIncomingCallForegroundServiceNotification(notification) } else { Log.w( "$TAG Failed to find notification for incoming call with ID [$INCOMING_CALL_ID]" @@ -763,7 +771,7 @@ class NotificationsManager val importance = channel?.importance ?: NotificationManagerCompat.IMPORTANCE_NONE if (importance == NotificationManagerCompat.IMPORTANCE_NONE) { Log.e("$TAG Calls channel has been disabled, can't start foreground service!") - stopInCallCallForegroundService() + stopInCallForegroundService() return } @@ -774,18 +782,29 @@ class NotificationsManager } else if (notificationsMap.containsKey(INCOMING_CALL_ID)) { notificationsMap[INCOMING_CALL_ID] } else { + Log.w("$TAG Failed to find a notification for call [${call.remoteAddress.asStringUriOnly()}] in map") null } - if (notification == null) { Log.w( "$TAG No existing notification (ID [$notificationId]) found for current call [${call.remoteAddress.asStringUriOnly()}], aborting" ) - stopInCallCallForegroundService() + stopInCallForegroundService() return } Log.i("$TAG Found notification [$notificationId] for current Call") + showInCallForegroundServiceNotification(call, notifiable, notification) + } + + @WorkerThread + private fun showInCallForegroundServiceNotification(call: Call, notifiable: Notifiable, notification: Notification) { + val service = inCallService + if (service == null) { + Log.w("$TAG Core Foreground Service hasn't started yet...") + return + } + var mask = Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL val callState = call.state if (!LinphoneUtils.isCallIncoming(callState) && !LinphoneUtils.isCallOutgoing(callState) && !LinphoneUtils.isCallEnding( @@ -830,11 +849,12 @@ class NotificationsManager notification, mask ) + notificationsMap[notifiable.notificationId] = notification currentInCallServiceNotificationId = notifiable.notificationId inCallServiceForegroundNotificationPublished = true if (waitForInCallServiceForegroundToStopIt) { Log.i("$TAG We were waiting for foreground service to be started to stop it, doing it") - stopInCallCallForegroundService() + stopInCallForegroundService() } } else { Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground service!") @@ -842,7 +862,7 @@ class NotificationsManager } @WorkerThread - private fun stopInCallCallForegroundService() { + private fun stopInCallForegroundService() { val service = inCallService if (service != null) { Log.i( @@ -851,6 +871,7 @@ class NotificationsManager service.stopForeground(STOP_FOREGROUND_REMOVE) service.stopSelf() inCallServiceForegroundNotificationPublished = false + waitForInCallServiceForegroundToStopIt = false } else { Log.w("$TAG Can't stop foreground Service & notif, no Service was found") } From 985a304df953a24746f151ff1107ac4c07280fb4 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 18 Apr 2025 11:06:19 +0200 Subject: [PATCH 109/593] Updated CHANGELOG & version code from release/6.0 branch --- CHANGELOG.md | 20 ++++++++++++++++++++ app/build.gradle.kts | 6 +++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0c3a0eb5d..c0cafcb0c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,26 @@ Group changes to describe their impact on the project, as follows: Fixed for any bug fixes. Security to invite users to upgrade in case of vulnerabilities. +## [6.1.0] - Unreleased + +### Added +- Added a vu meter for recording volume +- Added a setting for user to choose whether to sort contacts by first name or last name + +## [6.0.5] - 2025-04-18 + +### Changed +- When calling a SIP URI that looks like a phone number in the username and an IP in the domain, replace the domain with the one of the currently selected account to workaround issue with PBXs using IPs instead of domains in From header +- Improved account creation page UI when push notifications aren't available +- Improved called account display on incoming call screen when more than one account configured +- Updated telecom package from beta to release candidate + +### Fixed +- Fixed transfer call view numpad button starting a new call instead of forwarding the current one +- Fixed incoming call not displayed in call history depending on how the From & To headers are formatted (SDK fix) +- Fixed crashes related to foreground service not being started +- Fixed crash due to lateinit property not being initialized before used + ## [6.0.4] - 2025-04-11 ### Changed diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 01d0b20a4f..64f4badff6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,7 +32,7 @@ if (firebaseCloudMessagingAvailable) { } var gitBranch = ByteArrayOutputStream() -var gitVersion = "6.0.4" +var gitVersion = "6.0.5" task("getGitVersion") { val gitVersionStream = ByteArrayOutputStream() @@ -100,8 +100,8 @@ android { applicationId = packageName minSdk = 28 targetSdk = 36 - versionCode = 600004 // 6.00.004 - versionName = "6.0.4" + versionCode = 600005 // 6.00.005 + versionName = "6.0.5" manifestPlaceholders["appAuthRedirectScheme"] = packageName From 616b7bb70fbae0ed077efe275ca0d91ef113fa58 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 23 Apr 2025 09:06:51 +0200 Subject: [PATCH 110/593] Prevent crash & show error toast when trying to open a password protected PDF --- .../ui/fileviewer/viewmodel/FileViewModel.kt | 22 +++++++++++++------ app/src/main/res/values-fr/strings.xml | 5 +++-- app/src/main/res/values/strings.xml | 1 + 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt index d11a82b409..a623ee8b39 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt @@ -278,13 +278,21 @@ class FileViewModel File(filePath), ParcelFileDescriptor.MODE_READ_ONLY ) - pdfRenderer = PdfRenderer(input) - val count = pdfRenderer.pageCount - Log.i("$TAG $count pages in file $filePath") - pdfPages.postValue(count.toString()) - pdfCurrentPage.postValue("1") - pdfRendererReadyEvent.postValue(Event(true)) - fileReadyEvent.postValue(Event(true)) + try { + pdfRenderer = PdfRenderer(input) + val count = pdfRenderer.pageCount + Log.i("$TAG $count pages in file $filePath") + pdfPages.postValue(count.toString()) + pdfCurrentPage.postValue("1") + pdfRendererReadyEvent.postValue(Event(true)) + fileReadyEvent.postValue(Event(true)) + } catch (se: SecurityException) { + // TODO FIXME: add support for password protected PDFs + Log.e("$TAG Can't open PDF, probably protected by a password: $se") + pdfCurrentPage.postValue("0") + pdfPages.postValue("0") + showRedToast(R.string.conversation_pdf_file_cant_be_opened_error_toast, R.drawable.warning_circle) + } } } } diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index c94b56fd0d..3eb7db5bcc 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -513,7 +513,8 @@ Prendre une photo Ouvrir la gallerie Choisir un fichier - Impossible d\'ouvrir le fichier! + Impossible d\'ouvrir le fichier ! + Impossible d\'ouvrir un PDF protégé par mot de passe Participants (%s) Ajouter des participants @@ -524,7 +525,7 @@ Retirer les droits admin Voir le contact Ajouter aux contacts - Supprimer l\'historique ? + Supprimer l\'historique ? Tout les messages de cette conversation seront supprimés. Historique supprimé %s a rejoint la conversation diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9579b9ee2d..8ad92ceea1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -555,6 +555,7 @@ Open gallery Pick file File can\'t be opened! + Can\'t open password protected PDFs yet Group members (%s) Add participants From 90524da61090b84d4435272f57e437d6b09e54e2 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 23 Apr 2025 10:17:58 +0200 Subject: [PATCH 111/593] Fixed chat room lookup while in call --- .../ui/call/viewmodel/CurrentCallViewModel.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index c76b437716..eacde76aca 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -953,12 +953,10 @@ class CurrentCallViewModel fun createConversation() { if (::currentCall.isInitialized) { coreContext.postOnCoreThread { - val existingConversation = lookupCurrentCallConversation(currentCall) + val existingConversation = currentCallConversation ?: lookupCurrentCallConversation(currentCall) if (existingConversation != null) { Log.i( - "$TAG Found existing conversation [${ - LinphoneUtils.getConversationId(existingConversation) - }], going to it" + "$TAG Found existing conversation [${LinphoneUtils.getConversationId(existingConversation)}], going to it" ) goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(existingConversation))) } else { @@ -1390,8 +1388,9 @@ class CurrentCallViewModel val localAddress = call.callLog.localAddress val remoteAddress = call.remoteAddress - val params: ConferenceParams? = null val existingConversation = if (call.conference != null) { + Log.i("$TAG Looking for conversation with local address [${localAddress.asStringUriOnly()}] and peer address [${remoteAddress.asStringUriOnly()}]") + val params: ConferenceParams? = null // Don't need specific params, remote address should be enough in that scenario call.core.searchChatRoom( params, localAddress, @@ -1399,7 +1398,9 @@ class CurrentCallViewModel arrayOf() ) } else { + val params = getChatRoomParams(call) val participants = arrayOf(remoteAddress) + Log.i("$TAG Looking for conversation with local address [${localAddress.asStringUriOnly()}] and participant [${remoteAddress.asStringUriOnly()}]") call.core.searchChatRoom( params, localAddress, From f7790fbed7d4b9adf6b2b9be9004b5a2ffc83add Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 24 Apr 2025 09:32:03 +0200 Subject: [PATCH 112/593] Bumped AGP version & splashscreen dependency --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7eb414e425..4f4589cdac 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.9.1" +agp = "8.9.2" kotlin = "2.0.21" gmsGoogleServices = "4.4.2" firebaseCrashlytics = "3.0.3" @@ -11,7 +11,7 @@ activity = "1.10.1" appcompat = "1.7.0" constraintLayout = "2.2.1" coreKtx = "1.15.0" -splashscreen = "1.2.0-beta01" +splashscreen = "1.2.0-beta02" telecom = "1.0.0-rc01" media = "1.7.0" recyclerview = "1.4.0" From 6c86af747b5695952639fd0520d1c10ce76e952c Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 24 Apr 2025 10:04:10 +0200 Subject: [PATCH 113/593] Improved VFS confirmation dialog message --- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3eb7db5bcc..e3f2a25b62 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -185,7 +185,7 @@ Échec à l\'activation du module d\'encryption Module d\'encryption activé Confirmer l\'activation du chiffrement - Une fois la fonctionnalité activée, toutes les données de l\'application seront chiffrées et accessibles uniquement via celle-ci.\n\nCe changement est irréversible. + Une fois la fonctionnalité activée, vous devrez redémarrer l\'application.\nAprès ça toutes les données de l\'application seront chiffrées et accessibles uniquement via celle-ci.\n\nAttention, ce changement est irréversible ! Empêcher l\'interface d\'être enregistrée Appels Utiliser l\'annulateur d\'écho logiciel diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8ad92ceea1..ade21c3335 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -225,7 +225,7 @@ Failed to enable encryption module! Encryption module enabled Do you really want to encrypt everything? - Be careful, it can\'t be undone! + Once activated, you\'ll have to restart the app.\nAfter that all application data will be encrypted and accessible only via the application.\n\nBe careful, it can\'t be undone! Prevent interface from being recorded Calls Use software echo canceller From 056abd629f51816528eff342fbca37d4ea5b4de4 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 24 Apr 2025 10:10:15 +0200 Subject: [PATCH 114/593] Fixed newly created contact not appearing in contacts list --- app/src/main/java/org/linphone/contacts/ContactsManager.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 21130c6ed7..97e2f0e599 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -348,6 +348,7 @@ class ContactsManager for (sipAddress in friend.addresses) { newContactAddedWithSipUri(friend, sipAddress.asStringUriOnly()) } + notifyContactsListChanged() } @WorkerThread From 6e9c6d1b3391a8945ab4a5b5ee91f402328d6585 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 28 Apr 2025 11:26:35 +0200 Subject: [PATCH 115/593] Fixed group chat events icon --- .../org/linphone/ui/main/chat/model/EventModel.kt | 11 +++++++++-- .../chat/viewmodel/ConversationInfoViewModel.kt | 13 +++++++------ app/src/main/res/drawable/user_circle_dashed.xml | 9 +++++++++ app/src/main/res/drawable/user_circle_minus.xml | 9 +++++++++ app/src/main/res/drawable/user_circle_plus.xml | 9 +++++++++ 5 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 app/src/main/res/drawable/user_circle_dashed.xml create mode 100644 app/src/main/res/drawable/user_circle_minus.xml create mode 100644 app/src/main/res/drawable/user_circle_plus.xml diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/EventModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/EventModel.kt index eb7b28fdba..7dd85541a2 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/EventModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/EventModel.kt @@ -124,15 +124,22 @@ class EventModel R.drawable.pencil_simple } EventLog.Type.ConferenceCreated, - EventLog.Type.ConferenceParticipantAdded, - EventLog.Type.ConferenceParticipantRemoved, EventLog.Type.ConferenceParticipantDeviceAdded, EventLog.Type.ConferenceParticipantDeviceRemoved -> { R.drawable.door } + EventLog.Type.ConferenceParticipantAdded -> { + R.drawable.user_circle_plus + } + EventLog.Type.ConferenceParticipantRemoved -> { + R.drawable.user_circle_minus + } EventLog.Type.ConferenceParticipantSetAdmin -> { R.drawable.user_circle_check } + EventLog.Type.ConferenceParticipantUnsetAdmin -> { + R.drawable.user_circle_dashed + } else -> R.drawable.user_circle }, coreContext.context.theme diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt index ce12b415bb..dedac0fd27 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt @@ -110,7 +110,7 @@ class ConversationInfoViewModel R.string.conversation_info_participant_added_to_conversation_toast, getParticipant(eventLog) ) - showFormattedGreenToast(message, R.drawable.user_circle) + showFormattedGreenToast(message, R.drawable.user_circle_plus) computeParticipantsList() infoChangedEvent.postValue(Event(true)) @@ -123,7 +123,7 @@ class ConversationInfoViewModel R.string.conversation_info_participant_removed_from_conversation_toast, getParticipant(eventLog) ) - showFormattedGreenToast(message, R.drawable.user_circle) + showFormattedGreenToast(message, R.drawable.user_circle_minus) computeParticipantsList() infoChangedEvent.postValue(Event(true)) @@ -134,18 +134,19 @@ class ConversationInfoViewModel Log.i( "$TAG A participant has been given/removed administration rights for group [${chatRoom.subject}]" ) - val message = if (eventLog.type == EventLog.Type.ConferenceParticipantSetAdmin) { - AppUtils.getFormattedString( + if (eventLog.type == EventLog.Type.ConferenceParticipantSetAdmin) { + val message = AppUtils.getFormattedString( R.string.conversation_info_participant_has_been_granted_admin_rights_toast, getParticipant(eventLog) ) + showFormattedGreenToast(message, R.drawable.user_circle_check) } else { - AppUtils.getFormattedString( + val message = AppUtils.getFormattedString( R.string.conversation_info_participant_no_longer_has_admin_rights_toast, getParticipant(eventLog) ) + showFormattedGreenToast(message, R.drawable.user_circle_dashed) } - showFormattedGreenToast(message, R.drawable.user_circle) computeParticipantsList() } diff --git a/app/src/main/res/drawable/user_circle_dashed.xml b/app/src/main/res/drawable/user_circle_dashed.xml new file mode 100644 index 0000000000..b458710832 --- /dev/null +++ b/app/src/main/res/drawable/user_circle_dashed.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/user_circle_minus.xml b/app/src/main/res/drawable/user_circle_minus.xml new file mode 100644 index 0000000000..412154d176 --- /dev/null +++ b/app/src/main/res/drawable/user_circle_minus.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/user_circle_plus.xml b/app/src/main/res/drawable/user_circle_plus.xml new file mode 100644 index 0000000000..d9a95fc814 --- /dev/null +++ b/app/src/main/res/drawable/user_circle_plus.xml @@ -0,0 +1,9 @@ + + + From e5cec2d45c2a6ce3563944d168100a04e0258429 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 28 Apr 2025 15:34:33 +0200 Subject: [PATCH 116/593] Another attempt at fixing crashes related to in-call service never truly started as foreground before being stopped --- .../linphone/notifications/NotificationsManager.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 8b3809e3b7..f435290275 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -269,6 +269,9 @@ class NotificationsManager if (inCallServiceForegroundNotificationPublished) { Log.i("$TAG Stopping foreground service") stopInCallForegroundService() + } else { + Log.i("$TAG In-Call service was never started as foreground, waiting for it to be started to stop it") + waitForInCallServiceForegroundToStopIt = true } } @@ -517,8 +520,8 @@ class NotificationsManager @MainThread fun onInCallServiceDestroyed() { Log.i("$TAG Service has been destroyed") + stopInCallForegroundService() inCallService = null - currentInCallServiceNotificationId = -1 } @MainThread @@ -733,6 +736,8 @@ class NotificationsManager notificationsMap[INCOMING_CALL_ID] = notification currentInCallServiceNotificationId = INCOMING_CALL_ID inCallServiceForegroundNotificationPublished = true + Log.i("$TAG Incoming call notification with ID [$INCOMING_CALL_ID] has been used to start service as foreground") + if (waitForInCallServiceForegroundToStopIt) { Log.i("$TAG We were waiting for foreground service to be started to stop it, doing it") stopInCallForegroundService() @@ -852,6 +857,8 @@ class NotificationsManager notificationsMap[notifiable.notificationId] = notification currentInCallServiceNotificationId = notifiable.notificationId inCallServiceForegroundNotificationPublished = true + Log.i("$TAG Call notification with ID [${notifiable.notificationId}] has been used to start service as foreground") + if (waitForInCallServiceForegroundToStopIt) { Log.i("$TAG We were waiting for foreground service to be started to stop it, doing it") stopInCallForegroundService() @@ -861,7 +868,7 @@ class NotificationsManager } } - @WorkerThread + @AnyThread private fun stopInCallForegroundService() { val service = inCallService if (service != null) { From 1a813ee11e2eff0fd21c2c8b2ed8e53011714130 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 29 Apr 2025 09:33:38 +0200 Subject: [PATCH 117/593] Prevent crash due to uncaught exception --- .../java/org/linphone/compatibility/Api31Compatibility.kt | 4 +++- .../java/org/linphone/notifications/NotificationsManager.kt | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt index a84ddd1a53..121a47edd5 100644 --- a/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt @@ -47,7 +47,9 @@ class Api31Compatibility { .build() ) Log.i("$TAG PiP auto enter has been [${if (enable) "enabled" else "disabled"}]") - } catch (ise: IllegalArgumentException) { + } catch (iae: IllegalArgumentException) { + Log.e("$TAG Can't set PiP params: $iae") + } catch (ise: IllegalStateException) { Log.e("$TAG Can't set PiP params: $ise") } } diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index f435290275..1053a00068 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -1565,7 +1565,7 @@ class NotificationsManager val pendingIntent = TaskStackBuilder.create(context).run { addNextIntentWithParentStack( Intent(context, MainActivity::class.java).apply { - setAction(Intent.ACTION_MAIN) // Needed as well + action = Intent.ACTION_MAIN // Needed as well } ) getPendingIntent( From 2713c82ca3a7f3b16f3c80040a7b77dff618987a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 29 Apr 2025 10:07:34 +0200 Subject: [PATCH 118/593] Added content description french translation --- app/src/main/res/values-fr/strings.xml | 92 ++++++++++++++++++++++++++ app/src/main/res/values/strings.xml | 2 +- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index e3f2a25b62..07647ad897 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -813,4 +813,96 @@ La réunion a été mise à jour La réunion a été annulée + + + La confiance a été vérifiée avec tous les appareils du contact + Au moins un appareil du contact n\'est pas de confiance ! + Le contact est en ligne + Le contact est hors ligne + Ouvre le menu latéral + Retourne en arrière + Annule la notification + Sauvegarde les changements + Affiche le menu + Valide la nouvelle liste des participants + Cliquez pour en savoir plus + Cliquez pour retirer le participant + Alterne la visibilité du mot de passe + Alterne la visibilité du menu inférieur + Termine l\'appel + Décroche l\'appel + Décroche l\'appel en vidéo + Démarre un appel + Démarre un appel vidéo + Active ou désactive l\'envoi de votre flux vidéo + Activé ou désactive votre microphone + Change le périphérique de lecture du son + Change la caméra utilisée pour capturer la vidéo + L\'appel est en pause + Affiche les statistiques d\'appel + Vous êtes en train d\'enregistrer cet appel + Enlève le dernier caractère entré + Fusionne les appels en conférence + Ouvre la zone de filtre + Ferme la zone de filtre + Efface le filtre en cours + Créé une conversation de groupe + Démarre un appel de groupe + Affiche le pavé numérique + Cliquez pour voir toutes les options disponibles + Le participant a coupé son micro + Le participant est en train de parler + Ajoute des participants + Démarre ou arrête la lecture du son + Démarre ou arrête la lecture de la vidéo + Partager le fichier + Sauvegarder le fichier + Ce message contient une image + Ce message contient une vidéo + Ce message contient un fichier + Ce message est une réponse à un message précédent + Ce message a été transferré depuis une autre conversation + Cliquez pour afficher le status de réception + Démarre ou met en pause la lecture du message vocal + Retirer le fichier de la liste à envoyer + Ferme la liste des fichiers à envoyer + La conversation est mise en sourdine + Les messages éphémères sont activés + Scrolle vers le premier message non lu ou au dernier reçu + Ferme la liste des participants + Annule l\'enregistrement du message vocal + Arrête l\'enregistrement du message vocal + Démarre l\'enregistrement d\'un message vocal + Envoie le message + Le message ne sera plus une réponse à un précédent message + Ouvre le selectionneur d\'emoji + Ouvre le selectionneur de fichier + Cliquez pour modifier le sujet de la conversation + Met ou enlève la sourdine de la conversation + La conversation est en train d\'être supprimée + La conversation n\'est pas chiffrée de bout en bout + Rechercher vers le haut + Rechercher vers le bas + Démarrer une nouvelle conversation + Défiler jusqu\'à aujourd\'hui + Planifier une réunion + Planifier la réunion + Modifier la réunion + Partager l\'addresse de la réunion + Liste des participants + L\'appareil est de confiance + Modifier le contact + Retirer le champ + Afficher les filtres de la liste des contacts + Créer un contact + Rejoindre la conférence + Supprimer la configuration CardDAV + Sauvegarder la configuration CardDAV + Supprimer la configuration LDAP + Sauvegarder la configuration LDAP + Lancer la lecture de l\'enregistrement d\'appel + Aller à la conversation + Copier le texte dans le presse-papier + Au moins un message vocal est disponible + Faire un appui long pour appeler la boite vocale diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ade21c3335..a3a02f95f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -875,7 +875,7 @@ Answers the video call Starts a call Starts a video call - Enables/disables sending your camera feed + Enables/disables sending your camera feed Mute/un-mute your microphone Changes output audio device Changes camera (front/back) being used From 2634945b8d8fff51fa92b0616eebd7a6a95d45f6 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 29 Apr 2025 14:18:52 +0200 Subject: [PATCH 119/593] Improved chat room lookup while in conference --- .../linphone/ui/call/viewmodel/CurrentCallViewModel.kt | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index eacde76aca..69a69ad8e9 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -1389,14 +1389,8 @@ class CurrentCallViewModel val remoteAddress = call.remoteAddress val existingConversation = if (call.conference != null) { - Log.i("$TAG Looking for conversation with local address [${localAddress.asStringUriOnly()}] and peer address [${remoteAddress.asStringUriOnly()}]") - val params: ConferenceParams? = null // Don't need specific params, remote address should be enough in that scenario - call.core.searchChatRoom( - params, - localAddress, - remoteAddress, - arrayOf() - ) + Log.i("$TAG We're in [${remoteAddress.asStringUriOnly()}] conference, using it as chat room if possible") + call.conference?.chatRoom } else { val params = getChatRoomParams(call) val participants = arrayOf(remoteAddress) From 344afdfcfa164ea6d71f768fb71911b8d865473a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 29 Apr 2025 16:43:30 +0200 Subject: [PATCH 120/593] Moved print logs in logcat & file sharing URL settings to developper section, added logs upload file sharing server URL setting, added setting to disable crashlytics logs collection --- .../java/org/linphone/core/CoreContext.kt | 30 ++++-- .../java/org/linphone/core/CorePreferences.kt | 8 ++ .../ui/main/help/viewmodel/HelpViewModel.kt | 15 --- .../fragment/SettingsAdvancedFragment.kt | 1 - .../fragment/SettingsDeveloperFragment.kt | 6 ++ .../settings/viewmodel/SettingsViewModel.kt | 43 ++++++++- .../main/res/layout/help_debug_fragment.xml | 30 +----- .../res/layout/settings_advanced_fragment.xml | 66 ++++++------- .../layout/settings_developer_fragment.xml | 96 ++++++++++++++++++- app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 11 files changed, 209 insertions(+), 90 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 04a9f91303..efc4c07fce 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -495,6 +495,9 @@ class CoreContext private var logcatEnabled: Boolean = corePreferences.printLogsInLogcat + private var crashlyticsEnabled: Boolean = corePreferences.sendLogsToCrashlytics + private var crashlyticsAvailable = true + private val loggingServiceListener = object : LoggingServiceListenerStub() { @WorkerThread override fun onLogMessageWritten( @@ -512,7 +515,9 @@ class CoreContext else -> android.util.Log.d(domain, message) } } - FirebaseCrashlytics.getInstance().log("[$domain] [${level.name}] $message") + if (crashlyticsEnabled) { + FirebaseCrashlytics.getInstance().log("[$domain] [${level.name}] $message") + } } } @@ -532,9 +537,12 @@ class CoreContext Factory.instance().loggingService.addListener(loggingServiceListener) } catch (e: Exception) { Log.e("$TAG Failed to instantiate Crashlytics: $e") + crashlyticsEnabled = false + crashlyticsAvailable = false } } else { Log.i("$TAG Crashlytics is disabled") + crashlyticsAvailable = false } Log.i("=========================================") Log.i("==== Linphone-android information dump ====") @@ -1057,11 +1065,6 @@ class CoreContext core.setUserAgent(userAgent, sdkUserAgent) } - @WorkerThread - fun enableLogcat(enable: Boolean) { - logcatEnabled = enable - } - // Migration between versions related @WorkerThread @@ -1137,4 +1140,19 @@ class CoreContext Log.i("$TAG Removing previous grammar files (without .belr extension)") corePreferences.clearPreviousGrammars() } + + @WorkerThread + fun isCrashlyticsAvailable(): Boolean { + return crashlyticsAvailable + } + + @WorkerThread + fun updateLogcatEnabledSetting(enabled: Boolean) { + logcatEnabled = enabled + } + + @WorkerThread + fun updateCrashlyticsEnabledSetting(enabled: Boolean) { + crashlyticsEnabled = enabled + } } diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 76aa5d0d90..8c3b186a01 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -23,6 +23,7 @@ import android.content.Context import androidx.annotation.AnyThread import androidx.annotation.UiThread import androidx.annotation.WorkerThread +import org.linphone.BuildConfig import java.io.File import java.io.FileOutputStream import org.linphone.LinphoneApplication.Companion.coreContext @@ -53,6 +54,13 @@ class CorePreferences config.setBool("app", "debug", value) } + @get:WorkerThread @set:WorkerThread + var sendLogsToCrashlytics: Boolean + get() = config.getBool("app", "send_logs_to_crashlytics", BuildConfig.CRASHLYTICS_ENABLED) + set(value) { + config.setBool("app", "send_logs_to_crashlytics", value) + } + @get:WorkerThread @set:WorkerThread var firstLaunch: Boolean get() = config.getBool("app", "first_6.0_launch", true) diff --git a/app/src/main/java/org/linphone/ui/main/help/viewmodel/HelpViewModel.kt b/app/src/main/java/org/linphone/ui/main/help/viewmodel/HelpViewModel.kt index 3b33ae420b..8e43e7548a 100644 --- a/app/src/main/java/org/linphone/ui/main/help/viewmodel/HelpViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/help/viewmodel/HelpViewModel.kt @@ -32,7 +32,6 @@ import org.linphone.R import org.linphone.contacts.ContactLoader.Companion.NATIVE_ADDRESS_BOOK_FRIEND_LIST import org.linphone.core.Core import org.linphone.core.CoreListenerStub -import org.linphone.core.Factory import org.linphone.core.VersionUpdateCheckResult import org.linphone.core.tools.Log import org.linphone.ui.GenericViewModel @@ -47,8 +46,6 @@ class HelpViewModel private const val TAG = "[Help ViewModel]" } - val logcat = MutableLiveData() - val version = MutableLiveData() val appVersion = MutableLiveData() @@ -159,7 +156,6 @@ class HelpViewModel coreContext.postOnCoreThread { core -> core.addListener(coreListener) - logcat.postValue(corePreferences.printLogsInLogcat) checkUpdateAvailable.postValue(corePreferences.checkForUpdateServerUrl.isNotEmpty()) uploadLogsAvailable.postValue(!core.logCollectionUploadServerUrl.isNullOrEmpty()) } @@ -192,17 +188,6 @@ class HelpViewModel } } - @UiThread - fun toggleLogcat() { - val newValue = logcat.value == false - coreContext.postOnCoreThread { - corePreferences.printLogsInLogcat = newValue - coreContext.enableLogcat(newValue) - Factory.instance().enableLogcatLogs(newValue) - logcat.postValue(newValue) - } - } - @UiThread fun cleanLogs() { coreContext.postOnCoreThread { core -> diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedFragment.kt index bcd6c96aeb..b3e38aa445 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedFragment.kt @@ -117,7 +117,6 @@ class SettingsAdvancedFragment : GenericMainFragment() { override fun onPause() { viewModel.updateDeviceName() - viewModel.updateFileSharingServerUrl() viewModel.updateRemoteProvisioningUrl() super.onPause() diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsDeveloperFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsDeveloperFragment.kt index 36556bee07..6f0a0bcd33 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsDeveloperFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsDeveloperFragment.kt @@ -60,4 +60,10 @@ class SettingsDeveloperFragment : GenericMainFragment() { startPostponedEnterTransition() } + + override fun onPause() { + viewModel.updateSharingServersUrl() + + super.onPause() + } } diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 4b6ce43b03..3b59e3c820 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -197,12 +197,13 @@ class SettingsViewModel // Advanced settings val showAdvancedSettings = MutableLiveData() + val sendLogsToCrashlytics = MutableLiveData() + val isCrashlyticsAvailable = MutableLiveData() val startAtBoot = MutableLiveData() val keepAliveThirdPartyAccountsService = MutableLiveData() val useSmffForCallRecording = MutableLiveData() val deviceName = MutableLiveData() - val fileSharingServerUrl = MutableLiveData() val remoteProvisioningUrl = MutableLiveData() val expandAdvancedCalls = MutableLiveData() @@ -234,6 +235,9 @@ class SettingsViewModel // Developer settings val showDeveloperSettings = MutableLiveData() + val logcat = MutableLiveData() + val fileSharingServerUrl = MutableLiveData() + val logsSharingServerUrl = MutableLiveData() val createEndToEndEncryptedConferences = MutableLiveData() private val coreListener = object : CoreListenerStub() { @@ -257,6 +261,8 @@ class SettingsViewModel core.addListener(coreListener) isTunnelAvailable.postValue(core.tunnelAvailable()) + isCrashlyticsAvailable.postValue(coreContext.isCrashlyticsAvailable()) + showConversationsSettings.postValue(!corePreferences.disableChat) showMeetingsSettings.postValue(!corePreferences.disableMeetings) ldapAvailable.postValue(core.ldapAvailable()) @@ -338,11 +344,11 @@ class SettingsViewModel setupTunnel() } + sendLogsToCrashlytics.postValue(corePreferences.sendLogsToCrashlytics) startAtBoot.postValue(corePreferences.autoStart) keepAliveThirdPartyAccountsService.postValue(corePreferences.keepServiceAlive) deviceName.postValue(corePreferences.deviceName) - fileSharingServerUrl.postValue(core.fileTransferServer) remoteProvisioningUrl.postValue(core.provisioningUri) acceptEarlyMedia.postValue(corePreferences.acceptEarlyMedia) @@ -355,6 +361,9 @@ class SettingsViewModel setupAudioDevices() setupCodecs() + logcat.postValue(corePreferences.printLogsInLogcat) + fileSharingServerUrl.postValue(core.fileTransferServer) + logsSharingServerUrl.postValue(core.logCollectionUploadServerUrl) createEndToEndEncryptedConferences.postValue(corePreferences.createEndToEndEncryptedMeetingsAndGroupCalls) } } @@ -728,6 +737,17 @@ class SettingsViewModel } } + @UiThread + fun toggleSendLogsToCrashlytics() { + val newValue = sendLogsToCrashlytics.value == false + + coreContext.postOnCoreThread { + corePreferences.sendLogsToCrashlytics = newValue + sendLogsToCrashlytics.postValue(newValue) + coreContext.updateCrashlyticsEnabledSetting(newValue) + } + } + @UiThread fun toggleStartAtBoot() { val newValue = startAtBoot.value == false @@ -884,13 +904,19 @@ class SettingsViewModel } @UiThread - fun updateFileSharingServerUrl() { + fun updateSharingServersUrl() { coreContext.postOnCoreThread { core -> val newFileSharingServerUrl = fileSharingServerUrl.value.orEmpty().trim() if (newFileSharingServerUrl.isNotEmpty()) { Log.i("$TAG Updated file sharing server URL to [$newFileSharingServerUrl]") core.fileTransferServer = newFileSharingServerUrl } + + val newLogsSharingServerUrl = logsSharingServerUrl.value.orEmpty().trim() + if (newLogsSharingServerUrl.isNotEmpty()) { + Log.i("$TAG Updated logs upload server URL to [$newLogsSharingServerUrl]") + core.logCollectionUploadServerUrl = newLogsSharingServerUrl + } } } @@ -1064,6 +1090,17 @@ class SettingsViewModel } } + @UiThread + fun toggleLogcat() { + val newValue = logcat.value == false + coreContext.postOnCoreThread { + corePreferences.printLogsInLogcat = newValue + coreContext.updateLogcatEnabledSetting(newValue) + Factory.instance().enableLogcatLogs(newValue) + logcat.postValue(newValue) + } + } + @UiThread fun toggleConferencesEndToEndEncryption() { val newValue = createEndToEndEncryptedConferences.value == false diff --git a/app/src/main/res/layout/help_debug_fragment.xml b/app/src/main/res/layout/help_debug_fragment.xml index 0e7412ce65..8de0877225 100644 --- a/app/src/main/res/layout/help_debug_fragment.xml +++ b/app/src/main/res/layout/help_debug_fragment.xml @@ -64,34 +64,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/back" /> - - - - + app:layout_constraintTop_toBottomOf="@id/title" /> + + + + + app:layout_constraintTop_toBottomOf="@id/crashlytics_switch"/> - - - - + app:layout_constraintTop_toBottomOf="@id/device_id"/> + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/logs_upload_server_url" /> diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 07647ad897..643fb52cf6 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -263,11 +263,13 @@ Auto Paramètres avancés + Aider les développeurs à corriger les problèmes en envoyant les logs à Crashlytics après un crash Démarrer au lancement du téléphone Garder l\'app en vie via un Service Nom de l\'appareil Caractères alpha-numériques uniquement URL du serveur de partage de fichier + URL du serveur de partage des logs Enregistrer les appels vidéos utilisant H265/AV1 Utilisera un format de fichier propriétaire Chiffrement du média diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a3a02f95f8..54b631123e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -303,11 +303,13 @@ Auto Advanced settings + Help developers troubleshoot issues by sending logs to Crashlytics after a crash Start when device boots Keep app alive using Service Device ID Alpha-numerical characters only File sharing server URL + Logs sharing server URL Record video calls using H265/AV1 Will use a proprietary file format Media encryption From 966f713f198f2a58e9eea7e44c6d666596645345 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 29 Apr 2025 17:15:04 +0200 Subject: [PATCH 121/593] Increased margin for clean/share logs button in troubleshooting fragment --- app/src/main/res/layout/help_debug_fragment.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/help_debug_fragment.xml b/app/src/main/res/layout/help_debug_fragment.xml index 8de0877225..56e9745b1f 100644 --- a/app/src/main/res/layout/help_debug_fragment.xml +++ b/app/src/main/res/layout/help_debug_fragment.xml @@ -105,7 +105,7 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:layout_marginTop="10dp" + android:layout_marginTop="20dp" app:flow_wrapMode="chain" app:flow_horizontalGap="16dp" app:flow_verticalGap="10dp" From c9a3a01733d10465b9ce5422ff1e754e66c6c753 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 30 Apr 2025 15:40:49 +0200 Subject: [PATCH 122/593] Improved empty SIP contacts list --- .../contacts/fragment/ContactsListFragment.kt | 8 +++---- .../viewmodel/ContactsListViewModel.kt | 9 ++++---- .../layout-land/contacts_list_fragment.xml | 23 ++++++++++++++++++- .../res/layout/contacts_list_fragment.xml | 23 ++++++++++++++++++- app/src/main/res/values-fr/strings.xml | 2 ++ app/src/main/res/values/strings.xml | 2 ++ 6 files changed, 57 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt index cdf98eb857..858fb1456c 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt @@ -298,7 +298,7 @@ class ContactsListFragment : AbstractMainFragment() { null, false ) - popupView.seeAllSelected = listViewModel.areAllContactsDisplayed() + popupView.seeAllSelected = listViewModel.areAllContactsDisplayed.value == true popupView.showLinphoneFilter = listViewModel.isDefaultAccountLinphone.value == true val popupWindow = PopupWindow( @@ -309,7 +309,7 @@ class ContactsListFragment : AbstractMainFragment() { ) popupView.setNoFilterClickListener { - if (!listViewModel.areAllContactsDisplayed()) { + if (listViewModel.areAllContactsDisplayed.value != true) { listViewModel.changeContactsFilter( onlyLinphoneContacts = false, onlySipContacts = false @@ -319,7 +319,7 @@ class ContactsListFragment : AbstractMainFragment() { } popupView.setLinphoneOnlyClickListener { - if (listViewModel.areAllContactsDisplayed()) { + if (listViewModel.areAllContactsDisplayed.value == true) { listViewModel.changeContactsFilter( onlyLinphoneContacts = true, onlySipContacts = false @@ -329,7 +329,7 @@ class ContactsListFragment : AbstractMainFragment() { } popupView.setSipOnlyClickListener { - if (listViewModel.areAllContactsDisplayed()) { + if (listViewModel.areAllContactsDisplayed.value == true) { listViewModel.changeContactsFilter( onlyLinphoneContacts = false, onlySipContacts = true diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt index aa036ef4a1..7d9eb8a493 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt @@ -61,6 +61,8 @@ class ContactsListViewModel val isListFiltered = MutableLiveData() + val areAllContactsDisplayed = MutableLiveData() + val searchInProgress = MutableLiveData() val isDefaultAccountLinphone = MutableLiveData() @@ -107,6 +109,7 @@ class ContactsListViewModel coreContext.postOnCoreThread { core -> domainFilter = corePreferences.contactsFilter + areAllContactsDisplayed.postValue(domainFilter.isEmpty()) checkIfDefaultAccountOnDefaultDomain() coreContext.contactsManager.addListener(contactsListener) @@ -145,6 +148,7 @@ class ContactsListViewModel fun applyCurrentDefaultAccountFilter() { coreContext.postOnCoreThread { domainFilter = corePreferences.contactsFilter + areAllContactsDisplayed.postValue(domainFilter.isEmpty()) checkIfDefaultAccountOnDefaultDomain() coreContext.postOnMainThread { @@ -163,6 +167,7 @@ class ContactsListViewModel } else { "" } + areAllContactsDisplayed.postValue(domainFilter.isEmpty()) corePreferences.contactsFilter = domainFilter Log.i("$TAG Newly set filter is [${corePreferences.contactsFilter}]") @@ -172,10 +177,6 @@ class ContactsListViewModel } } - fun areAllContactsDisplayed(): Boolean { - return domainFilter.isEmpty() - } - @UiThread fun toggleFavouritesVisibility() { val show = showFavourites.value == false diff --git a/app/src/main/res/layout-land/contacts_list_fragment.xml b/app/src/main/res/layout-land/contacts_list_fragment.xml index 9f05a9b613..de9a6d2c7e 100644 --- a/app/src/main/res/layout-land/contacts_list_fragment.xml +++ b/app/src/main/res/layout-land/contacts_list_fragment.xml @@ -130,12 +130,33 @@ android:id="@+id/no_contacts_label" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@{viewModel.isFilterEmpty ? @string/contacts_list_empty : @string/list_filter_no_result_found, default=@string/contacts_list_empty}" + android:text="@{viewModel.isFilterEmpty ? (viewModel.areAllContactsDisplayed ? @string/contacts_list_empty : @string/contacts_list_sip_empty) : @string/list_filter_no_result_found, default=@string/contacts_list_sip_empty}" app:layout_constraintBottom_toTopOf="@id/lists" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/bottom_nav_bar" app:layout_constraintTop_toBottomOf="@id/no_contacts_image" /> + + + + Aucun contact pour le moment… + Aucun contact SIP pour le moment… + Changer le filtre Favoris Tous les contacts Tous les contacts diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 54b631123e..36962cc7c3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -413,6 +413,8 @@ No contact for the moment… + No SIP contact for the moment… + Change filter Favourites All contacts See all From 99a5ed23f6cd645b7e7d66f5f1fc5ee1df9fea10 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 2 May 2025 14:37:23 +0200 Subject: [PATCH 123/593] Updated CHANGELOG & version code from release/6.0 branch --- CHANGELOG.md | 19 +++++++++++++++++++ app/build.gradle.kts | 6 +++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0cafcb0c9..e7e53d9b23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,25 @@ Group changes to describe their impact on the project, as follows: - Added a vu meter for recording volume - Added a setting for user to choose whether to sort contacts by first name or last name +## [6.0.6] - 2025-05-02 + +### Added +- Added recover phone account when clicking on "Forgotten password" in the assistant +- Improved message when contacts list is empty depending on the currently selected filter and added a button to open the filter popup menu for users that didn't notice the icon on the top right corner of the screen when contacts list is empty and "SIP contacts only" filter is set. +- Added "Logs collection sharing server URL" setting in developper area +- Added "Disable sending logs to Crashlytics" advanced setting. + +### Changed +- Improved VFS message in confirmation dialog +- Moved "Print logs in logcat" and "File sharing server URL" settings to developper area + +### Fixed +- Fixed crash when opening a password protected PDF +- Fixed chat room lookup while in 1-1 call, using SDK method for getting chat room from conference +- Fixed newly created contact not being visible in contacts list without reloading it +- Fixed missing event icon for group conversations +- Another attempts at preventing crashes due to In-Call service not being started as foreground before being stopped + ## [6.0.5] - 2025-04-18 ### Changed diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 64f4badff6..5bc574602d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,7 +32,7 @@ if (firebaseCloudMessagingAvailable) { } var gitBranch = ByteArrayOutputStream() -var gitVersion = "6.0.5" +var gitVersion = "6.0.6" task("getGitVersion") { val gitVersionStream = ByteArrayOutputStream() @@ -100,8 +100,8 @@ android { applicationId = packageName minSdk = 28 targetSdk = 36 - versionCode = 600005 // 6.00.005 - versionName = "6.0.5" + versionCode = 600006 // 6.00.006 + versionName = "6.0.6" manifestPlaceholders["appAuthRedirectScheme"] = packageName From b7404096423c87cf36e383950174177919f6354a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 5 May 2025 12:01:23 +0200 Subject: [PATCH 124/593] Update conversations list after clearing conversation history --- .../linphone/ui/main/chat/fragment/ConversationInfoFragment.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt index 1f3a37892a..ba158ae8fe 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt @@ -149,6 +149,7 @@ class ConversationInfoFragment : SlidingPaneChildFragment() { viewModel.historyDeletedEvent.observe(viewLifecycleOwner) { it.consume { Log.i("$TAG History has been deleted, leaving conversation info...") + sharedViewModel.forceRefreshConversations.value = Event(true) sharedViewModel.forceRefreshConversationEvents.value = Event(true) goBack() val message = getString(R.string.conversation_info_history_deleted_toast) From e38040428bca57e11c6bdbea55b1d6fe49762b55 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 5 May 2025 13:45:01 +0200 Subject: [PATCH 125/593] Improved empty lists (contacts & conversations) labels + added button to let user know it can change account --- .../main/viewmodel/AbstractMainViewModel.kt | 13 ++++++++ .../res/layout-land/chat_list_fragment.xml | 33 ++++++++++++++++--- .../layout-land/contacts_list_fragment.xml | 6 ++-- .../res/layout-land/history_list_fragment.xml | 31 ++++++++++++++--- .../main/res/layout/chat_list_fragment.xml | 24 +++++++++++++- .../res/layout/contacts_list_fragment.xml | 3 +- app/src/main/res/layout/drawer_menu.xml | 14 ++------ .../main/res/layout/history_list_fragment.xml | 24 +++++++++++++- app/src/main/res/values-fr/strings.xml | 5 +-- app/src/main/res/values/strings.xml | 5 +-- 10 files changed, 128 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractMainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractMainViewModel.kt index 7482d4b92a..e81993a003 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractMainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractMainViewModel.kt @@ -70,6 +70,8 @@ open class AbstractMainViewModel val isFilterEmpty = MutableLiveData() + val moreThanOneAccount = MutableLiveData() + val focusSearchBarEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -146,6 +148,16 @@ open class AbstractMainViewModel } } + @WorkerThread + override fun onAccountAdded(core: Core, account: Account) { + moreThanOneAccount.postValue(core.accountList.size > 1) + } + + @WorkerThread + override fun onAccountRemoved(core: Core, account: Account) { + moreThanOneAccount.postValue(core.accountList.size > 1) + } + @WorkerThread override fun onDefaultAccountChanged(core: Core, defaultAccount: Account?) { updateAvailableMenus() @@ -175,6 +187,7 @@ open class AbstractMainViewModel hideMeetings.value = !coreContext.defaultAccountHasVideoConferenceFactoryUri coreContext.postOnCoreThread { core -> + moreThanOneAccount.postValue(core.accountList.size > 1) core.addListener(coreListener) configure() } diff --git a/app/src/main/res/layout-land/chat_list_fragment.xml b/app/src/main/res/layout-land/chat_list_fragment.xml index 164e0e38a4..b804759a37 100644 --- a/app/src/main/res/layout-land/chat_list_fragment.xml +++ b/app/src/main/res/layout-land/chat_list_fragment.xml @@ -74,22 +74,45 @@ android:src="@drawable/illu" android:contentDescription="@null" app:layout_constraintHeight_max="200dp" - app:layout_constraintBottom_toTopOf="@id/no_conversation_label" app:layout_constraintDimensionRatio="1:1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/bottom_nav_bar" - app:layout_constraintTop_toBottomOf="@id/background" /> + app:layout_constraintTop_toTopOf="@id/background" + app:layout_constraintBottom_toTopOf="@id/no_conversation_label" /> + app:layout_constraintTop_toBottomOf="@id/no_conversation_image" + app:layout_constraintBottom_toTopOf="@id/show_accounts_list"/> + + @@ -154,6 +155,7 @@ android:background="@drawable/secondary_button_background" android:visibility="@{viewModel.contactsList.empty && !viewModel.areAllContactsDisplayed ? View.VISIBLE : View.GONE}" app:layout_constraintTop_toBottomOf="@id/no_contacts_label" + app:layout_constraintBottom_toTopOf="@id/lists" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/bottom_nav_bar"/> diff --git a/app/src/main/res/layout-land/history_list_fragment.xml b/app/src/main/res/layout-land/history_list_fragment.xml index e279286d03..8ea1a46ef3 100644 --- a/app/src/main/res/layout-land/history_list_fragment.xml +++ b/app/src/main/res/layout-land/history_list_fragment.xml @@ -88,18 +88,41 @@ app:layout_constraintDimensionRatio="1:1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/bottom_nav_bar" - app:layout_constraintTop_toBottomOf="@id/background" /> + app:layout_constraintTop_toTopOf="@id/background" /> + app:layout_constraintTop_toBottomOf="@id/no_calls_image" + app:layout_constraintBottom_toTopOf="@id/show_accounts_list"/> + + + + + android:contentDescription="@null" /> + android:gravity="center" /> diff --git a/app/src/main/res/layout/history_list_fragment.xml b/app/src/main/res/layout/history_list_fragment.xml index 17f21ccaf3..b331ac4346 100644 --- a/app/src/main/res/layout/history_list_fragment.xml +++ b/app/src/main/res/layout/history_list_fragment.xml @@ -91,14 +91,36 @@ + + Aucun contact ni suggestion pour le moment… Nommer l\'appel de groupe Nom de l\'appel de groupe - Aucun appel dans votre historique… + Aucun appel vers/depuis ce compte n\'a été trouvé… + Changer de compte Conversation Supprimer l\'historique d\'appels ? @@ -431,7 +432,7 @@ Appareil sans nom - Aucune conversation pour le moment… + Aucune conversation liée à ce compte pour le moment… En cours de suppression… %s : diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 36962cc7c3..cbb1b586bf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -403,7 +403,8 @@ No suggestion and no contact for the moment… Set group call subject Group call subject - No call for the moment… + No call from/to this account was found… + Change account Conversation Do you really want to delete all calls history? @@ -472,7 +473,7 @@ Unnamed device - No conversation for the moment… + No conversation related to this account for the moment… Removal in progress… %s: From 4689b7c7da6208a68ce05c60d0ceca57f0a4e53d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 5 May 2025 14:49:07 +0200 Subject: [PATCH 126/593] Fixed app reloading lists too many times at startup when looking for friends in remote contact directories such as LDAP/CardDAV --- app/src/main/java/org/linphone/contacts/ContactsManager.kt | 1 - .../ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 97e2f0e599..21130c6ed7 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -348,7 +348,6 @@ class ContactsManager for (sipAddress in friend.addresses) { newContactAddedWithSipUri(friend, sipAddress.asStringUriOnly()) } - notifyContactsListChanged() } @WorkerThread diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt index c50f21f05c..72aa171143 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt @@ -250,6 +250,7 @@ class ContactNewOrEditViewModel } coreContext.contactsManager.newContactAdded(friend) + coreContext.contactsManager.notifyContactsListChanged() saveChangesEvent.postValue( Event(if (status == Status.OK) friend.refKey.orEmpty() else "") From b293bf7f2fe236c9090e8113211994d2de692d6d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 5 May 2025 15:42:36 +0200 Subject: [PATCH 127/593] Added back Weblate section to README --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 748fd7d8cd..8a7af8b502 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ -[![pipeline status](https://gitlab.linphone.org/BC/public/linphone-android/badges/master/pipeline.svg)](https://gitlab.linphone.org/BC/public/linphone-android/commits/master) [![weblate status](https://weblate.linphone.org/widgets/linphone/-/linphone-android/svg-badge.svg)](https://weblate.linphone.org/engage/linphone/?utm_source=widget) +[![pipeline status](https://gitlab.linphone.org/BC/public/linphone-android/badges/master/pipeline.svg)](https://gitlab.linphone.org/BC/public/linphone-android/commits/master) +[![weblate status](https://weblate.linphone.org/widget/linphone/linphone-android-6-0/status-badge.png)](https://weblate.linphone.org/engage/linphone/) Linphone is an open source softphone for voice and video over IP calling and instant messaging. @@ -161,6 +162,16 @@ If you delete it, you won't receive any push notification. If you have your own push server, replace this file by yours. +## Translations + +We no longer use transifex for the translation process, instead we have deployed our own instance of [Weblate](https://weblate.linphone.org/). + +Due to the full app rewrite we can't re-use previous translations, so we'll be very happy if you want to contribute. + + +Translation status + + # CONTRIBUTIONS In order to submit a patch for inclusion in linphone's source code: From 73237ee3353f69c7d26443346559fac1510ce91a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 6 May 2025 12:53:28 +0200 Subject: [PATCH 128/593] Added toast for meeting update error + fixed other toasts --- .../ui/call/viewmodel/CurrentCallViewModel.kt | 4 ++-- .../ui/main/contacts/viewmodel/ContactViewModel.kt | 4 ++-- .../meetings/viewmodel/ScheduleMeetingViewModel.kt | 12 +++++++++++- app/src/main/res/values-fr/strings.xml | 12 ++++++------ app/src/main/res/values/strings.xml | 2 +- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 69a69ad8e9..22a2ccab2b 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -421,7 +421,7 @@ class CurrentCallViewModel chatRoom.removeListener(this) operationInProgress.postValue(false) chatRoomCreationErrorEvent.postValue( - Event(R.string.conversation_creation_error_toast) + Event(R.string.conversation_failed_to_create_toast) ) } } @@ -1442,7 +1442,7 @@ class CurrentCallViewModel ) operationInProgress.postValue(false) chatRoomCreationErrorEvent.postValue( - Event(R.string.conversation_creation_error_toast) + Event(R.string.conversation_failed_to_create_toast) ) } } diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt index 2667c2d01b..5a16246b2d 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt @@ -211,7 +211,7 @@ class ContactViewModel chatRoom.removeListener(this) operationInProgress.postValue(false) chatRoomCreationErrorEvent.postValue( - Event(R.string.conversation_creation_error_toast) + Event(R.string.conversation_failed_to_create_toast) ) } } @@ -574,7 +574,7 @@ class ContactViewModel ) operationInProgress.postValue(false) chatRoomCreationErrorEvent.postValue( - Event(R.string.conversation_creation_error_toast) + Event(R.string.conversation_failed_to_create_toast) ) } } diff --git a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/ScheduleMeetingViewModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/ScheduleMeetingViewModel.kt index 880da234ab..50f2655dfb 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/ScheduleMeetingViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/ScheduleMeetingViewModel.kt @@ -113,7 +113,17 @@ class ScheduleMeetingViewModel when (state) { ConferenceScheduler.State.Error -> { operationInProgress.postValue(false) - showRedToast(R.string.meeting_failed_to_schedule_toast, R.drawable.warning_circle) + if (::conferenceInfo.isInitialized) { + showRedToast( + R.string.meeting_failed_to_edit_schedule_toast, + R.drawable.warning_circle + ) + } else { + showRedToast( + R.string.meeting_failed_to_schedule_toast, + R.drawable.warning_circle + ) + } } ConferenceScheduler.State.Ready -> { val conferenceAddress = conferenceScheduler.info?.uri diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index bfaf091569..d19c684c3e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -173,7 +173,7 @@ ID du projet Firebase Partager le lien vers journaux avec… Les journaux ont été nettoyés - Echec à l\'envoi des journaux + Échec à l\'envoi des journaux Afficher la configuration Supprimer les contacts natifs importés @@ -468,7 +468,6 @@ %s est en train d\'écrire… %s sont en train d\'écrire… - Échec de la création de la conversation ! Ajouter des participants En réponse à : Chercher @@ -617,6 +616,7 @@ Réunion annulée Réunion annulée Échec de création de la réunion ! + Échec de modification de la réunion ! Veuillez saisir un titre et sélectionner au moins un participant Échec de l\'envoi des invitations à la réunion ! Échec de l\'envoi des invitations à certains des participants ! @@ -626,7 +626,7 @@ Annuler Connexion à la réunion Vous allez rejoindre la réunion dans quelques instants… - Echec de connexion à la conférence! + Échec de connexion à la conférence! Appel sortant @@ -719,7 +719,7 @@ Appareil authentifié Transfert en cours Appel transféré - Echec du transfert + Échec du transfert Utilisateur occupé Utilisateur introuvable Paramètres media incompatibles @@ -732,8 +732,8 @@ En attente d\'autres participants… Partage d\'écran Participants - Echec de l\'appel de groupe - Echec de la fusion des appels + Échec de l\'appel de groupe + Échec de la fusion des appels Participant (%s) Participants (%s) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cbb1b586bf..e6a98591de 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -509,7 +509,6 @@ %s is composing… %s are composing… - Failed to create conversation! Add participants Replying to: Search @@ -658,6 +657,7 @@ Meeting has been cancelled Meeting cancelled Failed to schedule meeting! + Failed to edit meeting! Please fill the title and select at least one participant Failed to send all invites to meeting! Failed to send invites to some participants of the meeting! From a496e2bf56f13f930af9f2ff2032ed3675a9430f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 6 May 2025 14:33:19 +0200 Subject: [PATCH 129/593] Forgot to disable IMDN bottom sheet for incoming messages in groups --- app/src/main/res/layout/chat_bubble_incoming.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/chat_bubble_incoming.xml b/app/src/main/res/layout/chat_bubble_incoming.xml index dfb6637285..b87a4e8f14 100644 --- a/app/src/main/res/layout/chat_bubble_incoming.xml +++ b/app/src/main/res/layout/chat_bubble_incoming.xml @@ -236,6 +236,7 @@ android:id="@+id/date_time" android:onClick="@{showDeliveryInfoClickListener}" android:onLongClick="@{onLongClickListener}" + android:enabled="@{model.isFromGroup && !model.hideDeliveryStatus}" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{model.time, default=`13:40`}" From bcce9a9ba12d856df74e27676d9bc2649fef2339 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 7 May 2025 11:08:00 +0200 Subject: [PATCH 130/593] Updated AGP to 8.10 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4f4589cdac..fffc3a2569 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.9.2" +agp = "8.10.0" kotlin = "2.0.21" gmsGoogleServices = "4.4.2" firebaseCrashlytics = "3.0.3" From 1c1729f3f00f77d2212004e2edad13ac0c72089f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 7 May 2025 11:13:51 +0200 Subject: [PATCH 131/593] Fixed meeting list yesterday item still displayed as today if list isn't reloaded in two days --- .../linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt b/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt index 8153737769..44f567467e 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/adapter/MeetingsListAdapter.kt @@ -186,6 +186,9 @@ class MeetingsListAdapter : return oldItem.model.subject.value.orEmpty().isNotEmpty() && oldItem.model.subject.value == newItem.model.subject.value && oldItem.model.time == newItem.model.time && + oldItem.model.isCancelled == newItem.model.isCancelled && + oldItem.model.isToday == newItem.model.isToday && + oldItem.model.isAfterToday == newItem.model.isAfterToday && oldItem.firstMeetingOfTheWeek == newItem.firstMeetingOfTheWeek && oldItem.model.firstMeetingOfTheDay.value == newItem.model.firstMeetingOfTheDay.value } From 85e24e25bf20cc5b4aec979ec20b3de42ef52fb0 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 7 May 2025 16:12:34 +0200 Subject: [PATCH 132/593] Added missing toast events observer --- .../org/linphone/ui/main/history/fragment/HistoryListFragment.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt index 510a4aa73e..2cc9d6db50 100644 --- a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt @@ -103,6 +103,7 @@ class HistoryListFragment : AbstractMainFragment() { binding.lifecycleOwner = viewLifecycleOwner binding.viewModel = listViewModel + observeToastEvents(listViewModel) binding.historyList.setHasFixedSize(true) binding.historyList.layoutManager = LinearLayoutManager(requireContext()) From 926b8d4dc18fadeb1ffb023760544cfab91520c0 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 9 May 2025 11:09:20 +0200 Subject: [PATCH 133/593] Updated dependencies --- gradle/libs.versions.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fffc3a2569..5d470e40ee 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,23 +3,23 @@ agp = "8.10.0" kotlin = "2.0.21" gmsGoogleServices = "4.4.2" firebaseCrashlytics = "3.0.3" -firebaseBomVersion = "33.12.0" +firebaseBomVersion = "33.13.0" ktlint = "12.1.2" annotations = "1.9.1" activity = "1.10.1" appcompat = "1.7.0" constraintLayout = "2.2.1" -coreKtx = "1.15.0" +coreKtx = "1.16.0" splashscreen = "1.2.0-beta02" -telecom = "1.0.0-rc01" +telecom = "1.0.0" media = "1.7.0" recyclerview = "1.4.0" slidingpanelayout = "1.2.0" window = "1.3.0" gridlayout = "1.1.0" securityCryptoKtx = "1.1.0-alpha07" -navigation = "2.8.9" +navigation = "2.9.0" emoji2 = "1.5.0" car = "1.7.0-rc01" flexbox = "3.0.0" From d113797dfb50b0d79afbeeb15d3dcb5f285796d2 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 12 May 2025 09:56:38 +0200 Subject: [PATCH 134/593] Added auto answer with video in both directions advanced call setting --- .../java/org/linphone/core/CoreContext.kt | 12 ++++++-- .../java/org/linphone/core/CorePreferences.kt | 7 +++++ .../settings/viewmodel/SettingsViewModel.kt | 12 ++++++++ .../res/layout/settings_advanced_calls.xml | 30 +++++++++++++++++++ app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 60 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index efc4c07fce..c709667350 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -295,12 +295,12 @@ class CoreContext val autoAnswerDelay = corePreferences.autoAnswerDelay if (autoAnswerDelay == 0) { Log.w("$TAG Auto answering call immediately") - answerCall(call) + answerCall(call, true) } else { Log.i("$TAG Scheduling auto answering in $autoAnswerDelay milliseconds") postOnCoreThreadDelayed({ Log.w("$TAG Auto answering call") - answerCall(call) + answerCall(call, true) }, autoAnswerDelay.toLong()) } } @@ -916,7 +916,7 @@ class CoreContext } @WorkerThread - fun answerCall(call: Call) { + fun answerCall(call: Call, autoAnswer: Boolean = false) { Log.i( "$TAG Answering call with remote address [${call.remoteAddress.asStringUriOnly()}] and to address [${call.toAddress.asStringUriOnly()}]" ) @@ -942,6 +942,12 @@ class CoreContext Log.i( "$TAG Enabling video on call params to prevent audio-only layout when answering" ) + } else if (autoAnswer) { + val videoBothWays = corePreferences.autoAnswerVideoCallsWithVideoDirectionSendReceive + if (videoBothWays) { + Log.i("$TAG Call is being auto-answered, requesting video in both ways according to user setting") + params.videoDirection = MediaDirection.SendRecv + } } call.acceptWithParams(params) diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 8c3b186a01..b91feaefd5 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -183,6 +183,13 @@ class CorePreferences config.setInt("app", "auto_answer_delay", value) } + @get:WorkerThread @set:WorkerThread + var autoAnswerVideoCallsWithVideoDirectionSendReceive: Boolean + get() = config.getBool("app", "auto_answer_video_send_receive", false) + set(value) { + config.setBool("app", "auto_answer_video_send_receive", value) + } + // Conversation related @get:WorkerThread @set:WorkerThread diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 3b59e3c820..4586b21929 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -217,6 +217,7 @@ class SettingsViewModel val allowOutgoingEarlyMedia = MutableLiveData() val autoAnswerIncomingCalls = MutableLiveData() val autoAnswerIncomingCallsDelay = MutableLiveData() + val autoAnswerIncomingCallsWithVideoDirectionSendReceive = MutableLiveData() val expandAudioDevices = MutableLiveData() val inputAudioDeviceIndex = MutableLiveData() @@ -356,6 +357,7 @@ class SettingsViewModel allowOutgoingEarlyMedia.postValue(corePreferences.allowOutgoingEarlyMedia) autoAnswerIncomingCalls.postValue(corePreferences.autoAnswerEnabled) autoAnswerIncomingCallsDelay.postValue(corePreferences.autoAnswerDelay) + autoAnswerIncomingCallsWithVideoDirectionSendReceive.postValue(corePreferences.autoAnswerVideoCallsWithVideoDirectionSendReceive) setupMediaEncryption() setupAudioDevices() @@ -889,6 +891,16 @@ class SettingsViewModel } } + @UiThread + fun toggleEnableAutoAnswerIncomingCallsWithVideoDirectionSendReceive() { + val newValue = autoAnswerIncomingCallsWithVideoDirectionSendReceive.value == false + + coreContext.postOnCoreThread { core -> + corePreferences.autoAnswerVideoCallsWithVideoDirectionSendReceive = newValue + autoAnswerIncomingCallsWithVideoDirectionSendReceive.postValue(newValue) + } + } + @UiThread fun updateDeviceName() { coreContext.postOnCoreThread { diff --git a/app/src/main/res/layout/settings_advanced_calls.xml b/app/src/main/res/layout/settings_advanced_calls.xml index 241c37a2a9..636e03283e 100644 --- a/app/src/main/res/layout/settings_advanced_calls.xml +++ b/app/src/main/res/layout/settings_advanced_calls.xml @@ -317,6 +317,36 @@ app:layout_constraintStart_toStartOf="@id/auto_answer_incoming_calls_delay_title" app:layout_constraintEnd_toEndOf="parent"/> + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d19c684c3e..318cad9a4e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -281,6 +281,7 @@ Décrocher automatiquement les appels entrants Délai avant le décrochage automatique Délai en millisecondes + Décrocher automatiquement avec la vidéo activée dans les deux sens URL de configuration distante Télécharger & appliquer Périphériques audio diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e6a98591de..e4d24c0611 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -321,6 +321,7 @@ Auto answer incoming calls Delay before auto answering call Delay in milliseconds + Auto answer with video enabled in both directions Remote provisioning URL Download & apply Audio devices From 7c78b021db9a704f7b1c28b81c97d3fdf3726a6f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 12 May 2025 10:29:05 +0200 Subject: [PATCH 135/593] Removed some debug logs, improved findContactByAddress performances a bit --- .../org/linphone/contacts/ContactsManager.kt | 16 ++-------------- .../java/org/linphone/utils/LinphoneUtils.kt | 2 +- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 21130c6ed7..c3906e2658 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -417,16 +417,13 @@ class ContactsManager @WorkerThread fun findContactByAddress(address: Address): Friend? { - val sipUri = LinphoneUtils.getAddressAsCleanStringUriOnly(address) - Log.d("$TAG Looking for friend with SIP URI [$sipUri]") - - val username = address.username val found = coreContext.core.findFriend(address) if (found != null) { - Log.d("$TAG Friend [${found.name}] was found using SIP URI [$sipUri]") return found } + val username = address.username + val sipUri = LinphoneUtils.getAddressAsCleanStringUriOnly(address) // Start an async query in Magic Search in case LDAP or remote CardDAV is configured val remoteContactDirectories = coreContext.core.remoteContactDirectories if (remoteContactDirectories.isNotEmpty() && !magicSearchMap.keys.contains(sipUri) && !unknownRemoteContactDirectoriesContactsMap.contains( @@ -461,20 +458,11 @@ class ContactsManager Log.d("$TAG Looking for friend with phone number [$username]") val foundUsingPhoneNumber = coreContext.core.findFriendByPhoneNumber(username) if (foundUsingPhoneNumber != null) { - Log.d( - "$TAG Friend [${foundUsingPhoneNumber.name}] was found using phone number [$username]" - ) foundUsingPhoneNumber } else { - Log.d( - "$TAG Friend wasn't found using phone number [$username], looking in native address book directly" - ) null } } else { - Log.d( - "$TAG Friend wasn't found using SIP address [$sipAddress] and username [$username] isn't a phone number, looking in native address book directly" - ) null } } diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 24fb71d6b2..ead54682e5 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -86,7 +86,7 @@ class LinphoneUtils { fun getAddressAsCleanStringUriOnly(address: Address): String { val scheme = address.scheme ?: "sip" val username = address.username - if (username.orEmpty().isEmpty()) { + if (username.isNullOrEmpty()) { return "$scheme:${address.domain}" } return "$scheme:$username@${address.domain}" From dc2b94ca4d97e26c790c3a8aa0df49e05114a537 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 12 May 2025 12:00:33 +0200 Subject: [PATCH 136/593] Fixed broken link in README --- README.md | 2 +- .../java/org/linphone/contacts/ContactsManager.kt | 14 +------------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 8a7af8b502..b81fc5fadd 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Linphone is dual licensed, and is available either : ### Documentation -- Supported features and RFCs : https://linphone.org/technical-corner/linphone/features +- Supported features and RFCs : https://www.linphone.org/linphone-softphone/#linphone-fonctionnalites - Linphone public wiki : https://wiki.linphone.org/xwiki/wiki/public/view/Linphone/ diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index c3906e2658..9ce49e1b11 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -446,22 +446,10 @@ class ContactsManager ) } - val sipAddress = if (sipUri.startsWith("sip:")) { - sipUri.substring("sip:".length) - } else if (sipUri.startsWith("sips:")) { - sipUri.substring("sips:".length) - } else { - sipUri - } - return if (!username.isNullOrEmpty() && (username.startsWith("+") || username.isDigitsOnly())) { Log.d("$TAG Looking for friend with phone number [$username]") val foundUsingPhoneNumber = coreContext.core.findFriendByPhoneNumber(username) - if (foundUsingPhoneNumber != null) { - foundUsingPhoneNumber - } else { - null - } + foundUsingPhoneNumber } else { null } From 244061c0b10fb95217f7b2560d122943b51662a6 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 12 May 2025 16:10:56 +0200 Subject: [PATCH 137/593] Fixed black thumbnails when joining conference without bundle mode --- .../model/ConferenceParticipantDeviceModel.kt | 59 ++++++++----------- ...all_conference_active_speaker_fragment.xml | 4 +- .../call_conference_active_speaker_cell.xml | 4 +- ...all_conference_active_speaker_fragment.xml | 4 +- .../res/layout/call_conference_grid_cell.xml | 4 +- 5 files changed, 31 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/call/conference/model/ConferenceParticipantDeviceModel.kt b/app/src/main/java/org/linphone/ui/call/conference/model/ConferenceParticipantDeviceModel.kt index 18cb90cebf..73dde35895 100644 --- a/app/src/main/java/org/linphone/ui/call/conference/model/ConferenceParticipantDeviceModel.kt +++ b/app/src/main/java/org/linphone/ui/call/conference/model/ConferenceParticipantDeviceModel.kt @@ -24,7 +24,6 @@ import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.core.MediaDirection import org.linphone.core.ParticipantDevice import org.linphone.core.ParticipantDeviceListenerStub import org.linphone.core.StreamType @@ -57,7 +56,7 @@ class ConferenceParticipantDeviceModel val isVideoAvailable = MutableLiveData() - val isSendingVideo = MutableLiveData() + val isThumbnailAvailable = MutableLiveData() val isJoining = MutableLiveData() @@ -108,28 +107,6 @@ class ConferenceParticipantDeviceModel isSpeaking.postValue(speaking) } - @WorkerThread - override fun onStreamAvailabilityChanged( - participantDevice: ParticipantDevice, - available: Boolean, - streamType: StreamType? - ) { - Log.d( - "$TAG Participant device [${participantDevice.address.asStringUriOnly()}] stream [$streamType] availability changed to ${if (available) "available" else "not available"}" - ) - } - - @WorkerThread - override fun onStreamCapabilityChanged( - participantDevice: ParticipantDevice, - direction: MediaDirection?, - streamType: StreamType? - ) { - Log.d( - "$TAG Participant device [${participantDevice.address.asStringUriOnly()}] stream [$streamType] capability changed to [$direction]" - ) - } - @WorkerThread override fun onScreenSharingChanged( participantDevice: ParticipantDevice, @@ -149,19 +126,21 @@ class ConferenceParticipantDeviceModel Log.i( "$TAG Participant device [${participantDevice.address.asStringUriOnly()}] thumbnail availability changed to ${if (available) "available" else "not available"}" ) - isVideoAvailable.postValue(available) + isThumbnailAvailable.postValue(available) } @WorkerThread - override fun onThumbnailStreamCapabilityChanged( + override fun onStreamAvailabilityChanged( participantDevice: ParticipantDevice, - direction: MediaDirection? + available: Boolean, + streamType: StreamType? ) { - Log.i( - "$TAG Participant device [${participantDevice.address.asStringUriOnly()}] thumbnail capability changed to [$direction]" - ) - val sending = direction == MediaDirection.SendRecv || direction == MediaDirection.SendOnly - isSendingVideo.postValue(sending) + if (streamType == StreamType.Video) { + Log.i( + "$TAG Participant device [${participantDevice.address.asStringUriOnly()}] video stream availability changed to ${if (available) "available" else "not available"}" + ) + isVideoAvailable.postValue(available) + } } } @@ -196,10 +175,18 @@ class ConferenceParticipantDeviceModel Log.i("$TAG Participant [${device.address.asStringUriOnly()}] is sharing its screen") } - isVideoAvailable.postValue(device.getStreamAvailability(StreamType.Video)) - val videoCapability = device.getStreamCapability(StreamType.Video) - isSendingVideo.postValue( - videoCapability == MediaDirection.SendRecv || videoCapability == MediaDirection.SendOnly + val videoAvailability = device.getStreamAvailability(StreamType.Video) + isVideoAvailable.postValue(videoAvailability) + Log.i( + "$TAG Participant device [${device.address.asStringUriOnly()}] video stream availability is ${if (videoAvailability) "available" else "not available"}" + ) + + // In case of joining conference without bundle mode, thumbnail stream availability will be false, + // but we need to display our video preview for video stream to be sent + val thumbnailVideoAvailability = if (isMe) videoAvailability else device.thumbnailStreamAvailability + isThumbnailAvailable.postValue(thumbnailVideoAvailability) + Log.i( + "$TAG Participant device [${device.address.asStringUriOnly()}] thumbnail availability is ${if (thumbnailVideoAvailability) "available" else "not available"}" ) } diff --git a/app/src/main/res/layout-land/call_conference_active_speaker_fragment.xml b/app/src/main/res/layout-land/call_conference_active_speaker_fragment.xml index 04edd4d665..0744c8de89 100644 --- a/app/src/main/res/layout-land/call_conference_active_speaker_fragment.xml +++ b/app/src/main/res/layout-land/call_conference_active_speaker_fragment.xml @@ -27,7 +27,7 @@ layout="@layout/contact_avatar_huge" android:layout_marginTop="5dp" bind:model="@{conferenceViewModel.activeSpeaker.avatarModel}" - android:visibility="@{conferenceViewModel.activeSpeaker.isSendingVideo || conferenceViewModel.activeSpeaker.isJoining || !conferenceViewModel.activeSpeaker.isInConference ? View.GONE : View.VISIBLE}" + android:visibility="@{conferenceViewModel.activeSpeaker.isVideoAvailable || conferenceViewModel.activeSpeaker.isJoining || !conferenceViewModel.activeSpeaker.isInConference ? View.GONE : View.VISIBLE}" app:layout_constraintDimensionRatio="1:1" app:layout_constraintWidth_max="@dimen/avatar_in_call_size" app:layout_constraintHeight_max="@dimen/avatar_in_call_size" @@ -42,7 +42,7 @@ android:layout_width="0dp" android:layout_height="0dp" android:layout_marginEnd="8dp" - android:visibility="@{conferenceViewModel.activeSpeaker.isSendingVideo && conferenceViewModel.activeSpeaker.isInConference ? View.VISIBLE : View.GONE, default=gone}" + android:visibility="@{conferenceViewModel.activeSpeaker.isVideoAvailable && conferenceViewModel.activeSpeaker.isInConference ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/call_conference_active_speaker_cell.xml b/app/src/main/res/layout/call_conference_active_speaker_cell.xml index 146a27b120..39e961646a 100644 --- a/app/src/main/res/layout/call_conference_active_speaker_cell.xml +++ b/app/src/main/res/layout/call_conference_active_speaker_cell.xml @@ -27,7 +27,7 @@ android:id="@+id/avatar" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:visibility="@{model.isSendingVideo || model.isJoining || !model.isInConference ? View.GONE : View.VISIBLE}" + android:visibility="@{model.isThumbnailAvailable || model.isJoining || !model.isInConference ? View.GONE : View.VISIBLE}" layout="@layout/contact_avatar_medium" bind:model="@{model.avatarModel}" bind:hidePresence="@{true}" @@ -43,7 +43,7 @@ app:alignTopRight="false" app:displayMode="hybrid" participantTextureView="@{model}" - android:visibility="@{model.isSendingVideo && model.isInConference ? View.VISIBLE : View.GONE, default=gone}" + android:visibility="@{model.isThumbnailAvailable && model.isInConference ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/call_conference_active_speaker_fragment.xml b/app/src/main/res/layout/call_conference_active_speaker_fragment.xml index 421a6e6232..3d25fa6a07 100644 --- a/app/src/main/res/layout/call_conference_active_speaker_fragment.xml +++ b/app/src/main/res/layout/call_conference_active_speaker_fragment.xml @@ -27,7 +27,7 @@ layout="@layout/contact_avatar_huge" android:layout_marginTop="5dp" bind:model="@{conferenceViewModel.activeSpeaker.avatarModel}" - android:visibility="@{conferenceViewModel.activeSpeaker.isSendingVideo || conferenceViewModel.activeSpeaker.isJoining || !conferenceViewModel.activeSpeaker.isInConference ? View.GONE : View.VISIBLE}" + android:visibility="@{conferenceViewModel.activeSpeaker.isVideoAvailable || conferenceViewModel.activeSpeaker.isJoining || !conferenceViewModel.activeSpeaker.isInConference ? View.GONE : View.VISIBLE}" app:layout_constraintDimensionRatio="1:1" app:layout_constraintWidth_max="@dimen/avatar_in_call_size" app:layout_constraintHeight_max="@dimen/avatar_in_call_size" @@ -41,7 +41,7 @@ android:id="@+id/active_speaker_surface" android:layout_width="0dp" android:layout_height="0dp" - android:visibility="@{conferenceViewModel.activeSpeaker.isSendingVideo && conferenceViewModel.activeSpeaker.isInConference ? View.VISIBLE : View.GONE, default=gone}" + android:visibility="@{conferenceViewModel.activeSpeaker.isVideoAvailable && conferenceViewModel.activeSpeaker.isInConference ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@id/active_speaker_miniatures_horizontal_layout" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/call_conference_grid_cell.xml b/app/src/main/res/layout/call_conference_grid_cell.xml index 75204edc25..c09fda42ee 100644 --- a/app/src/main/res/layout/call_conference_grid_cell.xml +++ b/app/src/main/res/layout/call_conference_grid_cell.xml @@ -27,7 +27,7 @@ android:id="@+id/avatar" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:visibility="@{model.isSendingVideo || model.isJoining || !model.isInConference ? View.GONE : View.VISIBLE}" + android:visibility="@{model.isThumbnailAvailable || model.isJoining || !model.isInConference ? View.GONE : View.VISIBLE}" layout="@layout/contact_avatar_big" bind:model="@{model.avatarModel}" bind:hidePresence="@{true}" @@ -43,7 +43,7 @@ app:alignTopRight="false" app:displayMode="hybrid" participantTextureView="@{model}" - android:visibility="@{model.isSendingVideo && model.isInConference ? View.VISIBLE : View.GONE, default=gone}" + android:visibility="@{model.isThumbnailAvailable && model.isInConference ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" From 627f881364e4039fc3f5ab028bf563634fbbac85 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 13 May 2025 10:09:50 +0200 Subject: [PATCH 138/593] Make sure speaker audio device is used if available when incoming early media call is ringing --- app/src/main/java/org/linphone/core/CoreContext.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index c709667350..8055b60a75 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -305,6 +305,19 @@ class CoreContext } } } + Call.State.IncomingEarlyMedia -> { + if (core.ringDuringIncomingEarlyMedia) { + val speaker = core.audioDevices.find { + it.type == AudioDevice.Type.Speaker + } + if (speaker != null) { + Log.i("$TAG Ringing during incoming early media enabled, make sure speaker audio device [${speaker.id}] is used") + call.outputAudioDevice = speaker + } else { + Log.w("$TAG No speaker device found, incoming call early media ringing will be played on default device") + } + } + } Call.State.OutgoingInit -> { val conferenceInfo = core.findConferenceInformationFromUri(call.remoteAddress) // Do not show outgoing call view for conference calls, wait for connected state From d822cbc827edf254027c123b0f6067276eed8262 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 13 May 2025 10:15:42 +0200 Subject: [PATCH 139/593] Make sure after a remote provisioning a default account has been set --- app/src/main/java/org/linphone/core/CoreContext.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 8055b60a75..fdba233494 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -256,6 +256,16 @@ class CoreContext ) { Log.i("$TAG Configuring state changed [$status], message is [$message]") if (status == ConfiguringState.Successful) { + val accounts = core.accountList + if (core.defaultAccount == null && accounts.isNotEmpty()) { + val firstAccount = accounts.firstOrNull() + if (firstAccount != null) { + Log.w("$TAG Default account is null but account list isn't empty, using account [${firstAccount. + params.identityAddress?.asStringUriOnly()}] as default") + core.defaultAccount = firstAccount + } + } + provisioningAppliedEvent.postValue(Event(true)) corePreferences.firstLaunch = false showGreenToastEvent.postValue( From f1fdb186ecdc53a6af073db52dd1c2141dd4317c Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 13 May 2025 13:48:15 +0200 Subject: [PATCH 140/593] Reworked unread count indicators --- .../java/org/linphone/core/CoreContext.kt | 6 +++-- .../main/res/layout-land/bottom_nav_bar.xml | 23 ++++++------------- .../layout-land/call_actions_bottom_sheet.xml | 22 +++++------------- .../call_conference_actions_bottom_sheet.xml | 22 +++++------------- app/src/main/res/layout/account_list_cell.xml | 11 +++------ app/src/main/res/layout/bottom_nav_bar.xml | 22 +++++------------- .../res/layout/call_actions_bottom_sheet.xml | 22 +++++------------- .../call_conference_actions_bottom_sheet.xml | 22 +++++------------- .../res/layout/chat_conversation_fragment.xml | 10 +++----- app/src/main/res/layout/chat_list_cell.xml | 10 +++----- app/src/main/res/values/dimen.xml | 1 + app/src/main/res/values/styles.xml | 12 ++++++++++ 12 files changed, 63 insertions(+), 120 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index fdba233494..479e199791 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -260,8 +260,10 @@ class CoreContext if (core.defaultAccount == null && accounts.isNotEmpty()) { val firstAccount = accounts.firstOrNull() if (firstAccount != null) { - Log.w("$TAG Default account is null but account list isn't empty, using account [${firstAccount. - params.identityAddress?.asStringUriOnly()}] as default") + val sipUri = firstAccount.params.identityAddress?.asStringUriOnly() + Log.w( + "$TAG Default account is null but account list isn't empty, using account [$sipUri] as default" + ) core.defaultAccount = firstAccount } } diff --git a/app/src/main/res/layout-land/bottom_nav_bar.xml b/app/src/main/res/layout-land/bottom_nav_bar.xml index 727ac6e962..8ddb35647c 100644 --- a/app/src/main/res/layout-land/bottom_nav_bar.xml +++ b/app/src/main/res/layout-land/bottom_nav_bar.xml @@ -50,15 +50,11 @@ app:layout_constraintTop_toBottomOf="@id/contacts" /> @@ -81,16 +77,11 @@ app:layout_constraintTop_toBottomOf="@id/calls" /> diff --git a/app/src/main/res/layout-land/call_actions_bottom_sheet.xml b/app/src/main/res/layout-land/call_actions_bottom_sheet.xml index d8c0c2f144..377d5fc476 100644 --- a/app/src/main/res/layout-land/call_actions_bottom_sheet.xml +++ b/app/src/main/res/layout-land/call_actions_bottom_sheet.xml @@ -91,18 +91,13 @@ app:tint="@color/in_call_button_tint_color" /> diff --git a/app/src/main/res/layout/chat_list_cell.xml b/app/src/main/res/layout/chat_list_cell.xml index 77d4e4c90e..43375ce96d 100644 --- a/app/src/main/res/layout/chat_list_cell.xml +++ b/app/src/main/res/layout/chat_list_cell.xml @@ -144,16 +144,12 @@ app:layout_constraintBottom_toBottomOf="@id/title" /> 10dp 5dp 15dp + 24dp 300dp 300dp diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 5b54e32e71..b77910e78e 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -187,4 +187,16 @@ true centerCrop + From 6847227f1a0f7aec00f08d7c146c499dac1704c5 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 13 May 2025 14:02:45 +0200 Subject: [PATCH 141/593] Reworked click on SIP URI in chat message to prevent long press on it from starting the call --- .../chat/fragment/ConversationFragment.kt | 14 +++++++++++++ .../ui/main/chat/model/EventLogModel.kt | 2 ++ .../ui/main/chat/model/MessageModel.kt | 13 +++++------- .../chat/viewmodel/ConversationViewModel.kt | 21 ++++++++++++------- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index a9652e4686..3e7033a924 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -94,6 +94,7 @@ import org.linphone.utils.hideKeyboard import org.linphone.utils.setKeyboardInsetListener import org.linphone.utils.showKeyboard import androidx.core.net.toUri +import androidx.lifecycle.observe @UiThread open class ConversationFragment : SlidingPaneChildFragment() { @@ -766,6 +767,19 @@ open class ConversationFragment : SlidingPaneChildFragment() { } } + viewModel.sipUriToCallEvent.observe(viewLifecycleOwner) { + it.consume { sipUri -> + if (messageLongPressViewModel.visible.value == true) return@consume + val address = coreContext.core.interpretUrl(sipUri, false) + if (address != null) { + Log.i("$TAG Starting audio call to parsed SIP URI [${address.asStringUriOnly()}]") + coreContext.startAudioCall(address) + } else { + Log.w("$TAG Failed to parse [$sipUri] as SIP URI") + } + } + } + viewModel.conferenceToJoinEvent.observe(viewLifecycleOwner) { it.consume { conferenceUri -> if (messageLongPressViewModel.visible.value == true) return@consume diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/EventLogModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/EventLogModel.kt index 257c503579..08b08dcb91 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/EventLogModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/EventLogModel.kt @@ -34,6 +34,7 @@ class EventLogModel isGroupedWithNextOne: Boolean = false, currentFilter: String = "", onContentClicked: ((fileModel: FileModel) -> Unit)? = null, + onSipUriClicked: ((uri: String) -> Unit)? = null, onJoinConferenceClicked: ((uri: String) -> Unit)? = null, onWebUrlClicked: ((url: String) -> Unit)? = null, onContactClicked: ((friendRefKey: String) -> Unit)? = null, @@ -86,6 +87,7 @@ class EventLogModel isGroupedWithNextOne, currentFilter, onContentClicked, + onSipUriClicked, onJoinConferenceClicked, onWebUrlClicked, onContactClicked, diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt index 6f7021f0f9..904662abcb 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt @@ -79,6 +79,7 @@ class MessageModel isGroupedWithNextOne: Boolean, private val currentFilter: String = "", private val onContentClicked: ((fileModel: FileModel) -> Unit)? = null, + private val onSipUriClicked: ((uri: String) -> Unit)? = null, private val onJoinConferenceClicked: ((uri: String) -> Unit)? = null, private val onWebUrlClicked: ((url: String) -> Unit)? = null, private val onContactClicked: ((friendRefKey: String) -> Unit)? = null, @@ -673,8 +674,8 @@ class MessageModel spannableBuilder.replace(start, end, "@$displayName") val span = PatternClickableSpan.StyledClickableSpan( - object : - SpannableClickedListener { + object : SpannableClickedListener { + @UiThread override fun onSpanClicked(text: String) { val friendRefKey = friend.refKey ?: "" Log.i( @@ -707,12 +708,7 @@ class MessageModel override fun onSpanClicked(text: String) { coreContext.postOnCoreThread { Log.i("$TAG Clicked on SIP URI: $text") - val address = coreContext.core.interpretUrl(text, false) - if (address != null) { - coreContext.startAudioCall(address) - } else { - Log.w("$TAG Failed to parse [$text] as SIP URI") - } + onSipUriClicked?.invoke(text) } } } @@ -722,6 +718,7 @@ class MessageModel HTTP_LINK_REGEXP ), object : SpannableClickedListener { + @UiThread override fun onSpanClicked(text: String) { Log.i("$TAG Clicked on web URL: $text") onWebUrlClicked?.invoke(text) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt index 45c241ef76..4eacab00c5 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt @@ -111,6 +111,10 @@ class ConversationViewModel MutableLiveData>() } + val sipUriToCallEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + val conferenceToJoinEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -763,25 +767,28 @@ class ConversationViewModel index > 0, index != groupedEventLogs.size - 1, searchFilter.value.orEmpty(), - { fileModel -> + { fileModel -> // onContentClicked fileToDisplayEvent.postValue(Event(fileModel)) }, - { conferenceUri -> + { sipUri -> // onSipUriClicked + sipUriToCallEvent.postValue(Event(sipUri)) + }, + { conferenceUri -> // onJoinConferenceClicked conferenceToJoinEvent.postValue(Event(conferenceUri)) }, - { url -> + { url -> // onWebUrlClicked openWebBrowserEvent.postValue(Event(url)) }, - { friendRefKey -> + { friendRefKey -> // onContactClicked contactToDisplayEvent.postValue(Event(friendRefKey)) }, - { redToast -> + { redToast -> // onRedToastToShow showRedToastEvent.postValue(Event(redToast)) }, - { id -> + { id -> // onVoiceRecordingPlaybackEnded voiceRecordPlaybackEndedEvent.postValue(Event(id)) }, - { filePath -> + { filePath -> // onFileToExportToNativeGallery viewModelScope.launch { withContext(Dispatchers.IO) { Log.i("$TAG Export file [$filePath] to Android's MediaStore") From cfec621787f5dbd7d3a6b12f979214fb0c625bb7 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 13 May 2025 16:35:51 +0200 Subject: [PATCH 142/593] Fixed group chat event icons --- .../org/linphone/ui/main/chat/model/EventModel.kt | 14 ++++++++------ app/src/main/res/drawable/door_open.xml | 9 +++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 app/src/main/res/drawable/door_open.xml diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/EventModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/EventModel.kt index 7dd85541a2..4d508c61f8 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/EventModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/EventModel.kt @@ -116,22 +116,24 @@ class EventModel EventLog.Type.ConferenceEphemeralMessageLifetimeChanged -> { R.drawable.clock_countdown } - EventLog.Type.ConferenceTerminated, EventLog.Type.ConferenceSecurityEvent -> { R.drawable.warning_circle } EventLog.Type.ConferenceSubjectChanged -> { R.drawable.pencil_simple } - EventLog.Type.ConferenceCreated, - EventLog.Type.ConferenceParticipantDeviceAdded, - EventLog.Type.ConferenceParticipantDeviceRemoved -> { + EventLog.Type.ConferenceParticipantAdded, + EventLog.Type.ConferenceCreated -> { + R.drawable.door_open + } + EventLog.Type.ConferenceParticipantRemoved, + EventLog.Type.ConferenceTerminated -> { R.drawable.door } - EventLog.Type.ConferenceParticipantAdded -> { + EventLog.Type.ConferenceParticipantDeviceAdded -> { R.drawable.user_circle_plus } - EventLog.Type.ConferenceParticipantRemoved -> { + EventLog.Type.ConferenceParticipantDeviceRemoved -> { R.drawable.user_circle_minus } EventLog.Type.ConferenceParticipantSetAdmin -> { diff --git a/app/src/main/res/drawable/door_open.xml b/app/src/main/res/drawable/door_open.xml new file mode 100644 index 0000000000..30a950752a --- /dev/null +++ b/app/src/main/res/drawable/door_open.xml @@ -0,0 +1,9 @@ + + + From f5852a7b3ecc24bd084c437bb0c7c944f6524db4 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 14 May 2025 08:39:42 +0200 Subject: [PATCH 143/593] Prevent bottom nav bar titles from being cropped when font size is increased --- app/src/main/res/layout/bottom_nav_bar.xml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/app/src/main/res/layout/bottom_nav_bar.xml b/app/src/main/res/layout/bottom_nav_bar.xml index f0ad8f7b02..f77472237b 100644 --- a/app/src/main/res/layout/bottom_nav_bar.xml +++ b/app/src/main/res/layout/bottom_nav_bar.xml @@ -24,8 +24,6 @@ android:onClick="@{() -> viewModel.navigateToContacts()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:paddingTop="16dp" - android:paddingBottom="16dp" android:drawableTop="@drawable/address_book" android:drawablePadding="10dp" android:drawableTint="@{viewModel.contactsSelected ? @color/main1_500 : @color/main2_600, default=@color/main2_600}" @@ -43,8 +41,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="18dp" - android:paddingTop="16dp" - android:paddingBottom="16dp" android:drawableTop="@drawable/phone" android:drawablePadding="10dp" android:drawableTint="@{viewModel.callsSelected ? @color/main1_500 : @color/main2_600, default=@color/main2_600}" @@ -75,8 +71,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="18dp" - android:paddingTop="16dp" - android:paddingBottom="16dp" android:drawableTop="@drawable/chat_teardrop_text" android:drawablePadding="10dp" android:drawableTint="@{viewModel.conversationsSelected ? @color/main1_500 : @color/main2_600, default=@color/main2_600}" @@ -108,8 +102,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="18dp" - android:paddingTop="16dp" - android:paddingBottom="16dp" android:drawableTop="@drawable/video_conference" android:drawablePadding="10dp" android:drawableTint="@{viewModel.meetingsSelected ? @color/main1_500 : @color/main2_600, default=@color/main2_600}" From 02cbb45de9ddd24d8144277152f1c5377c2a5e4b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 13 May 2025 17:02:30 +0200 Subject: [PATCH 144/593] Trying to prevent another race condition in notifications manager leading to foreground service not being started before being stopped --- .../notifications/NotificationsManager.kt | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 1053a00068..c75dd5ecaf 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -99,6 +99,7 @@ class NotificationsManager const val CHAT_NOTIFICATIONS_GROUP = "CHAT_NOTIF_GROUP" private const val INCOMING_CALL_ID = 1 + private const val DUMMY_NOTIF_ID = 3 private const val KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID = 5 private const val MISSED_CALL_ID = 10 } @@ -502,8 +503,14 @@ class NotificationsManager coreContext.postOnCoreThread { core -> if (core.callsNb == 0) { Log.w("$TAG No call anymore, stopping service") + if (waitForInCallServiceForegroundToStopIt) { + Log.w("$TAG Service wasn't started as foreground yet, doing it now using a dummy notification") + showDummyNotificationForCallService() + } if (inCallServiceForegroundNotificationPublished) { stopInCallForegroundService() + } else { + Log.w("$TAG Foreground service notification wasn't published, shouldn't happen") } } else if (currentInCallServiceNotificationId == -1) { val call = core.currentCall ?: core.calls.first() @@ -868,6 +875,55 @@ class NotificationsManager } } + @AnyThread + private fun showDummyNotificationForCallService() { + val service = inCallService + if (service != null) { + val channelId = context.getString(R.string.notification_channel_call_id) + val pendingIntent = TaskStackBuilder.create(context).run { + addNextIntentWithParentStack( + Intent(context, MainActivity::class.java).apply { + action = Intent.ACTION_MAIN // Needed as well + } + ) + getPendingIntent( + KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + )!! + } + val builder = NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.linphone_notification) + .setAutoCancel(false) + .setOngoing(true) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setShowWhen(false) + .setContentIntent(pendingIntent) + val notification = builder.build() + + if (Compatibility.isPostNotificationsPermissionGranted(context)) { + Log.i( + "$TAG Service found, starting it as foreground using dummy notification ID [$DUMMY_NOTIF_ID]" + ) + Compatibility.startServiceForeground( + service, + DUMMY_NOTIF_ID, + notification, + Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL + ) + notificationsMap[INCOMING_CALL_ID] = notification + currentInCallServiceNotificationId = DUMMY_NOTIF_ID + inCallServiceForegroundNotificationPublished = true + Log.i("$TAG Dummy notification with ID [$DUMMY_NOTIF_ID] has been used to start service as foreground") + } else { + Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground service!") + } + } else { + Log.w("$TAG Core Foreground Service hasn't started yet...") + } + } + @AnyThread private fun stopInCallForegroundService() { val service = inCallService From 502c6413eec5500211e90697751fe6dcec1c2b02 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 14 May 2025 09:29:01 +0200 Subject: [PATCH 145/593] Reworked bottom nav bar (in portrait) --- app/src/main/res/layout/bottom_nav_bar.xml | 29 +++++++++++-------- .../main/res/layout/chat_list_fragment.xml | 2 +- .../res/layout/contacts_list_fragment.xml | 2 +- .../main/res/layout/history_list_fragment.xml | 2 +- .../res/layout/meetings_list_fragment.xml | 2 +- app/src/main/res/values/dimen.xml | 1 - 6 files changed, 21 insertions(+), 17 deletions(-) diff --git a/app/src/main/res/layout/bottom_nav_bar.xml b/app/src/main/res/layout/bottom_nav_bar.xml index f77472237b..d8f82ffee3 100644 --- a/app/src/main/res/layout/bottom_nav_bar.xml +++ b/app/src/main/res/layout/bottom_nav_bar.xml @@ -14,7 +14,7 @@ @@ -24,8 +24,10 @@ android:onClick="@{() -> viewModel.navigateToContacts()}" android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_marginTop="12dp" + android:layout_marginBottom="12dp" android:drawableTop="@drawable/address_book" - android:drawablePadding="10dp" + android:drawablePadding="4dp" android:drawableTint="@{viewModel.contactsSelected ? @color/main1_500 : @color/main2_600, default=@color/main2_600}" android:text="@string/bottom_navigation_contacts_label" textFont="@{viewModel.contactsSelected ? NotoSansFont.NotoSansBold : NotoSansFont.NotoSansRegular}" @@ -40,9 +42,10 @@ android:onClick="@{() -> viewModel.navigateToHistory()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="18dp" + android:layout_marginTop="12dp" + android:layout_marginBottom="12dp" android:drawableTop="@drawable/phone" - android:drawablePadding="10dp" + android:drawablePadding="4dp" android:drawableTint="@{viewModel.callsSelected ? @color/main1_500 : @color/main2_600, default=@color/main2_600}" android:text="@string/bottom_navigation_calls_label" textFont="@{viewModel.callsSelected ? NotoSansFont.NotoSansBold : NotoSansFont.NotoSansRegular}" @@ -57,10 +60,10 @@ android:layout_width="@dimen/unread_count_indicator_size" android:layout_height="@dimen/unread_count_indicator_size" android:layout_marginStart="50dp" - android:layout_marginTop="5dp" + android:layout_marginTop="2dp" android:text="@{String.valueOf(viewModel.missedCallsCount), default=`1`}" android:visibility="@{viewModel.missedCallsCount > 0 ? View.VISIBLE : View.GONE}" - app:layout_constraintTop_toTopOf="@id/calls" + app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="@id/calls" app:layout_constraintEnd_toEndOf="@id/calls"/> @@ -70,9 +73,10 @@ android:onClick="@{() -> viewModel.navigateToConversations()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="18dp" + android:layout_marginTop="12dp" + android:layout_marginBottom="12dp" android:drawableTop="@drawable/chat_teardrop_text" - android:drawablePadding="10dp" + android:drawablePadding="4dp" android:drawableTint="@{viewModel.conversationsSelected ? @color/main1_500 : @color/main2_600, default=@color/main2_600}" android:text="@string/bottom_navigation_conversations_label" android:visibility="@{viewModel.hideConversations ? View.GONE : View.VISIBLE}" @@ -88,10 +92,10 @@ android:layout_width="@dimen/unread_count_indicator_size" android:layout_height="@dimen/unread_count_indicator_size" android:layout_marginStart="50dp" - android:layout_marginTop="5dp" + android:layout_marginTop="2dp" android:text="@{String.valueOf(viewModel.unreadMessages), default=`1`}" android:visibility="@{viewModel.unreadMessages > 0 && !viewModel.hideConversations ? View.VISIBLE : View.GONE}" - app:layout_constraintTop_toTopOf="@id/conversations" + app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="@id/conversations" app:layout_constraintEnd_toEndOf="@id/conversations"/> @@ -101,9 +105,10 @@ android:onClick="@{() -> viewModel.navigateToMeetings()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="18dp" + android:layout_marginTop="12dp" + android:layout_marginBottom="12dp" android:drawableTop="@drawable/video_conference" - android:drawablePadding="10dp" + android:drawablePadding="4dp" android:drawableTint="@{viewModel.meetingsSelected ? @color/main1_500 : @color/main2_600, default=@color/main2_600}" android:text="@string/bottom_navigation_meetings_label" android:visibility="@{viewModel.hideMeetings ? View.GONE : View.VISIBLE}" diff --git a/app/src/main/res/layout/chat_list_fragment.xml b/app/src/main/res/layout/chat_list_fragment.xml index ccee01ac35..7e6dfcc3c0 100644 --- a/app/src/main/res/layout/chat_list_fragment.xml +++ b/app/src/main/res/layout/chat_list_fragment.xml @@ -126,7 +126,7 @@ diff --git a/app/src/main/res/layout/contacts_list_fragment.xml b/app/src/main/res/layout/contacts_list_fragment.xml index 9597bf2a49..4dc30a0b68 100644 --- a/app/src/main/res/layout/contacts_list_fragment.xml +++ b/app/src/main/res/layout/contacts_list_fragment.xml @@ -174,7 +174,7 @@ diff --git a/app/src/main/res/layout/history_list_fragment.xml b/app/src/main/res/layout/history_list_fragment.xml index b331ac4346..f13b1c3e3f 100644 --- a/app/src/main/res/layout/history_list_fragment.xml +++ b/app/src/main/res/layout/history_list_fragment.xml @@ -136,7 +136,7 @@ diff --git a/app/src/main/res/layout/meetings_list_fragment.xml b/app/src/main/res/layout/meetings_list_fragment.xml index ad654234f9..c789d793a4 100644 --- a/app/src/main/res/layout/meetings_list_fragment.xml +++ b/app/src/main/res/layout/meetings_list_fragment.xml @@ -79,7 +79,7 @@ diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index f44b6a4122..5acd61d935 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -5,7 +5,6 @@ 10dp - 85dp 75dp 350dp 300dp From 17588de5a956a5d0a139ab87fb7e83bb2f474019 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 15 May 2025 11:48:04 +0200 Subject: [PATCH 146/593] Do not delete chat rooms when removing account, will cause leaving groups in case of multi device --- .../ui/main/settings/viewmodel/AccountProfileViewModel.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt index 6012728e8d..f11c396565 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt @@ -271,9 +271,13 @@ class AccountProfileViewModel Log.i("$TAG Removing call logs, conversations & meetings related to account being removed") account.clearCallLogs() + // Wait for a better API in the SDK, deleteChatRoom will cause user to leave the groups, + // which will cause issues in case of multi device + /* for (conversation in account.chatRooms) { core.deleteChatRoom(conversation) } + */ for (meeting in account.conferenceInformationList) { core.deleteConferenceInformation(meeting) } From 1c3173b8712cfed876e18b3d17abc5775f3165d6 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 15 May 2025 09:58:01 +0200 Subject: [PATCH 147/593] Moved call related advanced parameters into dedicated fragment --- .../fragment/SettingsAdvancedCallFragment.kt | 92 +++++ .../fragment/SettingsAdvancedFragment.kt | 26 -- .../settings/fragment/SettingsFragment.kt | 7 + .../settings/viewmodel/SettingsViewModel.kt | 15 + .../res/layout/settings_advanced_calls.xml | 352 ------------------ .../settings_advanced_calls_auto_answer.xml | 112 ++++++ .../settings_advanced_calls_early_media.xml | 107 ++++++ .../settings_advanced_calls_fragment.xml | 347 +++++++++++++++++ .../res/layout/settings_advanced_fragment.xml | 107 +----- app/src/main/res/layout/settings_calls.xml | 22 ++ app/src/main/res/layout/settings_fragment.xml | 4 + .../main/res/navigation/main_nav_graph.xml | 17 +- app/src/main/res/values-fr/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 14 files changed, 733 insertions(+), 481 deletions(-) create mode 100644 app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedCallFragment.kt delete mode 100644 app/src/main/res/layout/settings_advanced_calls.xml create mode 100644 app/src/main/res/layout/settings_advanced_calls_auto_answer.xml create mode 100644 app/src/main/res/layout/settings_advanced_calls_early_media.xml create mode 100644 app/src/main/res/layout/settings_advanced_calls_fragment.xml diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedCallFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedCallFragment.kt new file mode 100644 index 0000000000..c5ca37b063 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedCallFragment.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2010-2025 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.ui.main.settings.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import androidx.annotation.UiThread +import androidx.lifecycle.ViewModelProvider +import org.linphone.R +import org.linphone.databinding.SettingsAdvancedCallsFragmentBinding +import org.linphone.ui.main.fragment.GenericMainFragment +import org.linphone.ui.main.settings.viewmodel.SettingsViewModel + +@UiThread +class SettingsAdvancedCallFragment : GenericMainFragment() { + private lateinit var binding: SettingsAdvancedCallsFragmentBinding + + private lateinit var viewModel: SettingsViewModel + + private val mediaEncryptionDropdownListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + viewModel.setMediaEncryption(position) + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = SettingsAdvancedCallsFragmentBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + postponeEnterTransition() + super.onViewCreated(view, savedInstanceState) + + viewModel = ViewModelProvider(this)[SettingsViewModel::class.java] + + binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel + observeToastEvents(viewModel) + + binding.setBackClickListener { + goBack() + } + + viewModel.mediaEncryptionIndex.observe(viewLifecycleOwner) { + setupMediaEncryptionPicker() + } + + startPostponedEnterTransition() + } + + private fun setupMediaEncryptionPicker() { + val index = viewModel.mediaEncryptionIndex.value ?: 0 + val adapter = ArrayAdapter( + requireContext(), + R.layout.drop_down_item, + viewModel.mediaEncryptionLabels + ) + adapter.setDropDownViewResource(R.layout.generic_dropdown_cell) + binding.mediaEncryption.adapter = adapter + binding.mediaEncryption.onItemSelectedListener = mediaEncryptionDropdownListener + binding.mediaEncryption.setSelection(index) + } +} diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedFragment.kt index b3e38aa445..f1cc856a02 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsAdvancedFragment.kt @@ -40,15 +40,6 @@ class SettingsAdvancedFragment : GenericMainFragment() { private lateinit var viewModel: SettingsViewModel - private val mediaEncryptionDropdownListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - viewModel.setMediaEncryption(position) - } - - override fun onNothingSelected(parent: AdapterView<*>?) { - } - } - private val inputAudioDeviceDropdownListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { viewModel.setInputAudioDevice(position) @@ -94,10 +85,6 @@ class SettingsAdvancedFragment : GenericMainFragment() { (requireActivity() as GenericActivity).goToAndroidPermissionSettings() } - viewModel.mediaEncryptionIndex.observe(viewLifecycleOwner) { - setupMediaEncryptionPicker() - } - viewModel.inputAudioDeviceIndex.observe(viewLifecycleOwner) { setupInputAudioDevicePicker() } @@ -122,19 +109,6 @@ class SettingsAdvancedFragment : GenericMainFragment() { super.onPause() } - private fun setupMediaEncryptionPicker() { - val index = viewModel.mediaEncryptionIndex.value ?: 0 - val adapter = ArrayAdapter( - requireContext(), - R.layout.drop_down_item, - viewModel.mediaEncryptionLabels - ) - adapter.setDropDownViewResource(R.layout.generic_dropdown_cell) - binding.advancedCallsSettings.mediaEncryption.adapter = adapter - binding.advancedCallsSettings.mediaEncryption.onItemSelectedListener = mediaEncryptionDropdownListener - binding.advancedCallsSettings.mediaEncryption.setSelection(index) - } - private fun setupInputAudioDevicePicker() { val index = viewModel.inputAudioDeviceIndex.value ?: 0 val adapter = ArrayAdapter( diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt index b3c1afa9ee..071adca755 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt @@ -148,6 +148,13 @@ class SettingsFragment : GenericMainFragment() { goBack() } + binding.setAdvancedCallSettingsClickListener { + if (findNavController().currentDestination?.id == R.id.settingsFragment) { + val action = SettingsFragmentDirections.actionSettingsFragmentToSettingsAdvancedCallFragment() + findNavController().navigate(action) + } + } + binding.setAdvancedSettingsClickListener { if (findNavController().currentDestination?.id == R.id.settingsFragment) { val action = SettingsFragmentDirections.actionSettingsFragmentToSettingsAdvancedFragment() diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 4586b21929..0fc439ecfb 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -233,6 +233,9 @@ class SettingsViewModel val expandVideoCodecs = MutableLiveData() val videoCodecs = MutableLiveData>() + val expandEarlyMedia = MutableLiveData() + val expandAutoAnswer = MutableLiveData() + // Developer settings val showDeveloperSettings = MutableLiveData() @@ -286,6 +289,8 @@ class SettingsViewModel expandAudioDevices.value = false expandAudioCodecs.value = false expandVideoCodecs.value = false + expandEarlyMedia.value = false + expandAutoAnswer.value = false val vfsEnabled = VFS.isEnabled(coreContext.context) isVfsEnabled.value = vfsEnabled @@ -839,6 +844,11 @@ class SettingsViewModel } } + @UiThread + fun toggleEarlyMediaExpand() { + expandEarlyMedia.value = expandEarlyMedia.value == false + } + @UiThread fun toggleAcceptEarlyMedia() { val newValue = acceptEarlyMedia.value == false @@ -869,6 +879,11 @@ class SettingsViewModel } } + @UiThread + fun toggleAutoAnswerExpand() { + expandAutoAnswer.value = expandAutoAnswer.value == false + } + @UiThread fun toggleEnableAutoAnswerIncomingCalls() { val newValue = autoAnswerIncomingCalls.value == false diff --git a/app/src/main/res/layout/settings_advanced_calls.xml b/app/src/main/res/layout/settings_advanced_calls.xml deleted file mode 100644 index 636e03283e..0000000000 --- a/app/src/main/res/layout/settings_advanced_calls.xml +++ /dev/null @@ -1,352 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/settings_advanced_calls_auto_answer.xml b/app/src/main/res/layout/settings_advanced_calls_auto_answer.xml new file mode 100644 index 0000000000..f5b948fc30 --- /dev/null +++ b/app/src/main/res/layout/settings_advanced_calls_auto_answer.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/settings_advanced_calls_early_media.xml b/app/src/main/res/layout/settings_advanced_calls_early_media.xml new file mode 100644 index 0000000000..8f264bcd4d --- /dev/null +++ b/app/src/main/res/layout/settings_advanced_calls_early_media.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/settings_advanced_calls_fragment.xml b/app/src/main/res/layout/settings_advanced_calls_fragment.xml new file mode 100644 index 0000000000..61bd69ae5d --- /dev/null +++ b/app/src/main/res/layout/settings_advanced_calls_fragment.xml @@ -0,0 +1,347 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/settings_advanced_fragment.xml b/app/src/main/res/layout/settings_advanced_fragment.xml index f9708e1b50..6000c756c2 100644 --- a/app/src/main/res/layout/settings_advanced_fragment.xml +++ b/app/src/main/res/layout/settings_advanced_fragment.xml @@ -1,7 +1,6 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> @@ -232,35 +231,6 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/remote_provisioning"/> - - - - + app:layout_constraintTop_toBottomOf="@id/download_and_apply"/> @@ -397,74 +367,6 @@ - - - - - - - - diff --git a/app/src/main/res/layout/settings_calls.xml b/app/src/main/res/layout/settings_calls.xml index 43fe0e7791..1566fd8c4f 100644 --- a/app/src/main/res/layout/settings_calls.xml +++ b/app/src/main/res/layout/settings_calls.xml @@ -5,6 +5,9 @@ + @@ -225,6 +228,25 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/auto_record_switch"/> + + \ 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 c9515b7597..30e17770f2 100644 --- a/app/src/main/res/layout/settings_fragment.xml +++ b/app/src/main/res/layout/settings_fragment.xml @@ -11,6 +11,9 @@ + @@ -124,6 +127,7 @@ android:layout_marginEnd="16dp" android:visibility="@{viewModel.expandCalls ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintTop_toBottomOf="@id/calls" + bind:advancedCallSettingsClickListener="@{advancedCallSettingsClickListener}" bind:viewModel="@{viewModel}"/> + + tools:layout="@layout/settings_advanced_fragment"> + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 318cad9a4e..b6adebfefc 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -270,14 +270,17 @@ Caractères alpha-numériques uniquement URL du serveur de partage de fichier URL du serveur de partage des logs + Paramètres d\'appels avancés Enregistrer les appels vidéos utilisant H265/AV1 Utilisera un format de fichier propriétaire Chiffrement du média Rendre le chiffrement du média obligatoire Créer en mode chiffré de bout en bout les réunions et les appels de groupe + Early media Accepter l\'early media Sonner pendant un appel entrant avec early-media Autoriser l\'early media pour les appels sortants + Décrochage automatique Décrocher automatiquement les appels entrants Délai avant le décrochage automatique Délai en millisecondes diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e4d24c0611..464ab90207 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -310,14 +310,17 @@ Alpha-numerical characters only File sharing server URL Logs sharing server URL + Advanced calls settings Record video calls using H265/AV1 Will use a proprietary file format Media encryption Media encryption mandatory Create end-to-end encrypted meetings & group calls + Early-media Accept early media Ring during incoming early media call Allow outgoing early media + Auto-answer Auto answer incoming calls Delay before auto answering call Delay in milliseconds From 28b6bd7e90311ef2a42b93403a8c36d88c5186d8 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 16 May 2025 11:13:58 +0200 Subject: [PATCH 148/593] Updated changelog & version code/name from release/6.0 branch --- CHANGELOG.md | 20 ++++++++++++++++++++ app/build.gradle.kts | 6 +++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7e53d9b23..1bd8139d05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,26 @@ Group changes to describe their impact on the project, as follows: - Added a vu meter for recording volume - Added a setting for user to choose whether to sort contacts by first name or last name +## [6.0.7] - 2025-05-16 + +### Added +- CS, NL and RU translations from Weblate + +### Changed +- Improved find contact performances +- Make sure speaker audio device is used for playing the ringtone during early media +- Reworked bottom navigation bar in portrait and unread count indicators +- No longer delete conversations when deleting account (for now); causes user to leave group which is an issue when using multiple devices + +### Fixed +- Fixed no default account after remote provisioning +- Prevent lists from refreshing too many times when using LDAP or remote CardDAV contact directories +- Fixed black miniatures in conference if bundle mode is disabled in account params +- Fixed long press on a chat message containing a SIP URI triggering call +- Disable IMDN bottom sheet for incoming messages in groups instead of showing it empty +- Refresh conversations list after clearing conversation history +- Fixed another race condition issue related to foreground call service + ## [6.0.6] - 2025-05-02 ### Added diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5bc574602d..a39ed72b0c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,7 +32,7 @@ if (firebaseCloudMessagingAvailable) { } var gitBranch = ByteArrayOutputStream() -var gitVersion = "6.0.6" +var gitVersion = "6.0.7" task("getGitVersion") { val gitVersionStream = ByteArrayOutputStream() @@ -100,8 +100,8 @@ android { applicationId = packageName minSdk = 28 targetSdk = 36 - versionCode = 600006 // 6.00.006 - versionName = "6.0.6" + versionCode = 600007 // 6.00.007 + versionName = "6.0.7" manifestPlaceholders["appAuthRedirectScheme"] = packageName From 1556abc79e257bc09d5f8784e98931061a40e595 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 16 May 2025 11:19:45 +0200 Subject: [PATCH 149/593] Fixed logs sharing server URL setting --- CHANGELOG.md | 4 ++++ app/src/main/res/layout/settings_developer_fragment.xml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bd8139d05..2c962dc70d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ Group changes to describe their impact on the project, as follows: ### Added - Added a vu meter for recording volume - Added a setting for user to choose whether to sort contacts by first name or last name +- Added a setting to let app auto-answer call with video sending already enabled + +### Changed +- Reworked some settings (moved calls related ones from advanced settings to advanced calls settings) ## [6.0.7] - 2025-05-16 diff --git a/app/src/main/res/layout/settings_developer_fragment.xml b/app/src/main/res/layout/settings_developer_fragment.xml index 602f51c86e..65cd5901b5 100644 --- a/app/src/main/res/layout/settings_developer_fragment.xml +++ b/app/src/main/res/layout/settings_developer_fragment.xml @@ -167,7 +167,7 @@ android:layout_marginEnd="16dp" android:paddingStart="20dp" android:paddingEnd="20dp" - android:text="@={viewModel.fileSharingServerUrl}" + android:text="@={viewModel.logsSharingServerUrl}" android:textSize="14sp" android:maxLines="1" android:background="@drawable/edit_text_background" From 81d0da4241e5f2f9b56769f6c3ed9cae7d341d9e Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 16 May 2025 12:14:37 +0200 Subject: [PATCH 150/593] Updated coil dependency --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5d470e40ee..d75f064617 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,7 +25,7 @@ car = "1.7.0-rc01" flexbox = "3.0.0" material = "1.12.0" protobuf = "3.25.5" -coil = "3.1.0" +coil = "3.2.0" dotsIndicator = "5.1.0" photoview = "2.3.0" openidAppauth = "0.11.1" From 25d13f44c70511f6a93c0c9ce58990aacd3de466 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 16 May 2025 15:51:28 +0200 Subject: [PATCH 151/593] Prevent 1-1 events for conference joined/left + temporary read only state --- .../linphone/ui/main/chat/model/ConversationModel.kt | 2 +- .../ui/main/chat/viewmodel/ConversationViewModel.kt | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt index d6a09e53bc..0917992731 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt @@ -115,7 +115,7 @@ class ConversationModel @WorkerThread override fun onConferenceLeft(chatRoom: ChatRoom, eventLog: EventLog) { Log.w("TAG Conversation has been left") - isReadOnly.postValue(true) + isReadOnly.postValue(chatRoom.isReadOnly) } @WorkerThread diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt index 4eacab00c5..92e963311e 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt @@ -153,7 +153,9 @@ class ConversationViewModel @WorkerThread override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) { Log.i("$TAG Conversation was joined") - addEvents(arrayOf(eventLog)) + if (LinphoneUtils.isChatRoomAGroup(chatRoom)) { + addEvents(arrayOf(eventLog)) + } computeConversationInfo() val messageToForward = pendingForwardMessage @@ -167,8 +169,10 @@ class ConversationViewModel @WorkerThread override fun onConferenceLeft(chatRoom: ChatRoom, eventLog: EventLog) { Log.w("$TAG Conversation was left") - addEvents(arrayOf(eventLog)) - isReadOnly.postValue(true) + if (LinphoneUtils.isChatRoomAGroup(chatRoom)) { + addEvents(arrayOf(eventLog)) + } + isReadOnly.postValue(chatRoom.isReadOnly) } @WorkerThread From 7aae03f1f98e9f04746301bbf9571f7c3a0d9ea0 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 19 May 2025 13:37:45 +0200 Subject: [PATCH 152/593] Fixed missing margin in media grid for audio files --- app/src/main/res/layout/chat_media_content_grid_cell.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/chat_media_content_grid_cell.xml b/app/src/main/res/layout/chat_media_content_grid_cell.xml index 1703fdcc59..8c028699a7 100644 --- a/app/src/main/res/layout/chat_media_content_grid_cell.xml +++ b/app/src/main/res/layout/chat_media_content_grid_cell.xml @@ -51,6 +51,7 @@ android:id="@+id/audio_file" android:layout_width="0dp" android:layout_height="0dp" + android:layout_margin="1dp" android:adjustViewBounds="true" android:padding="18dp" android:background="@drawable/shape_squircle_main2_200" From 4cb7ea19655b5be3a14748a4fab07d5bfa49bd1c Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 19 May 2025 13:09:10 +0200 Subject: [PATCH 153/593] Showing files in square area like media when more than one in a single chat message --- .../chat/adapter/ConversationsFilesAdapter.kt | 13 +- .../chat/fragment/ConversationFragment.kt | 38 ++-- .../linphone/ui/main/chat/model/FileModel.kt | 1 - .../ui/main/chat/model/MessageModel.kt | 13 +- .../SendMessageInConversationViewModel.kt | 45 ++-- .../org/linphone/utils/DataBindingUtils.kt | 2 +- .../shape_squircle_file_background_top.xml | 5 + .../shape_squircle_file_bubble_background.xml | 5 + ...squircle_file_bubble_right_background.xml} | 2 +- ...shape_squircle_gray_200_r15_background.xml | 5 + .../layout/chat_bubble_content_grid_cell.xml | 203 ++---------------- .../res/layout/chat_bubble_file_grid_cell.xml | 155 +++++++++++++ .../main/res/layout/chat_bubble_incoming.xml | 12 +- .../layout/chat_bubble_media_grid_cell.xml | 79 +++++++ .../main/res/layout/chat_bubble_outgoing.xml | 12 +- .../chat_bubble_single_file_content.xml | 165 ++++++++++++++ .../chat_bubble_single_media_content.xml | 8 +- ...hat_conversation_attachments_area_cell.xml | 102 ++------- .../chat_document_content_list_cell.xml | 67 ------ .../layout/chat_media_content_grid_cell.xml | 114 +++++++--- app/src/main/res/values/dimen.xml | 6 +- 21 files changed, 616 insertions(+), 436 deletions(-) create mode 100644 app/src/main/res/drawable/shape_squircle_file_background_top.xml create mode 100644 app/src/main/res/drawable/shape_squircle_file_bubble_background.xml rename app/src/main/res/drawable/{shape_squircle_white_right.xml => shape_squircle_file_bubble_right_background.xml} (80%) create mode 100644 app/src/main/res/drawable/shape_squircle_gray_200_r15_background.xml create mode 100644 app/src/main/res/layout/chat_bubble_file_grid_cell.xml create mode 100644 app/src/main/res/layout/chat_bubble_media_grid_cell.xml create mode 100644 app/src/main/res/layout/chat_bubble_single_file_content.xml delete mode 100644 app/src/main/res/layout/chat_document_content_list_cell.xml diff --git a/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationsFilesAdapter.kt b/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationsFilesAdapter.kt index 298d7af90b..de508a05c8 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationsFilesAdapter.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationsFilesAdapter.kt @@ -30,10 +30,11 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import org.linphone.R -import org.linphone.databinding.ChatDocumentContentListCellBinding +import org.linphone.databinding.ChatBubbleSingleFileContentBinding import org.linphone.databinding.ChatMediaContentGridCellBinding import org.linphone.databinding.MeetingsListDecorationBinding import org.linphone.ui.main.chat.model.FileModel +import org.linphone.utils.AppUtils import org.linphone.utils.HeaderAdapter class ConversationsFilesAdapter : @@ -46,6 +47,9 @@ class ConversationsFilesAdapter : const val DOCUMENT_FILE = 2 } + private val topBottomPadding = AppUtils.getDimension(R.dimen.chat_documents_list_padding_top_bottom).toInt() + private val startEndPadding = AppUtils.getDimension(R.dimen.chat_documents_list_padding_start_end).toInt() + override fun displayHeaderForPosition(position: Int): Boolean { if (position == 0) return true @@ -89,15 +93,16 @@ class ConversationsFilesAdapter : } private fun createDocumentFileViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { - val binding: ChatDocumentContentListCellBinding = DataBindingUtil.inflate( + val binding: ChatBubbleSingleFileContentBinding = DataBindingUtil.inflate( LayoutInflater.from(parent.context), - R.layout.chat_document_content_list_cell, + R.layout.chat_bubble_single_file_content, parent, false ) val viewHolder = DocumentFileViewHolder(binding) binding.apply { lifecycleOwner = parent.findViewTreeLifecycleOwner() + root.setPadding(startEndPadding, topBottomPadding, startEndPadding, topBottomPadding) } return viewHolder } @@ -123,7 +128,7 @@ class ConversationsFilesAdapter : } inner class DocumentFileViewHolder( - val binding: ChatDocumentContentListCellBinding + val binding: ChatBubbleSingleFileContentBinding ) : RecyclerView.ViewHolder(binding.root) { @UiThread fun bind(fileModel: FileModel) { diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 3e7033a924..0e082fd0b6 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -128,22 +128,20 @@ open class ConversationFragment : SlidingPaneChildFragment() { ) ) { list -> sendMessageViewModel.closeFilePickerBottomSheet() - if (list.isNotEmpty()) { + val filesToAttach = arrayListOf() + lifecycleScope.launch { for (uri in list) { - lifecycleScope.launch { - withContext(Dispatchers.IO) { - val path = FileUtils.getFilePath(requireContext(), uri, false) - Log.i("$TAG Picked file [$uri] matching path is [$path]") - if (path != null) { - withContext(Dispatchers.Main) { - sendMessageViewModel.addAttachment(path) - } - } + withContext(Dispatchers.IO) { + val path = FileUtils.getFilePath(requireContext(), uri, false) + Log.i("$TAG Picked file [$uri] matching path is [$path]") + if (path != null) { + filesToAttach.add(path) } } } - } else { - Log.w("$TAG No file picked") + withContext(Dispatchers.Main) { + sendMessageViewModel.addAttachments(filesToAttach) + } } } @@ -153,16 +151,20 @@ open class ConversationFragment : SlidingPaneChildFragment() { ActivityResultContracts.OpenMultipleDocuments() ) { files -> sendMessageViewModel.closeFilePickerBottomSheet() - for (fileUri in files) { - lifecycleScope.launch { + val filesToAttach = arrayListOf() + lifecycleScope.launch { + for (fileUri in files) { val path = FileUtils.getFilePath(requireContext(), fileUri, false).orEmpty() if (path.isNotEmpty()) { Log.i("$TAG Picked file [$path]") - sendMessageViewModel.addAttachment(path) + filesToAttach.add(path) } else { Log.e("$TAG Failed to pick file [$fileUri]") } } + withContext(Dispatchers.Main) { + sendMessageViewModel.addAttachments(filesToAttach) + } } } @@ -174,7 +176,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { if (path != null) { if (captured) { Log.i("$TAG Image was captured and saved in [$path]") - sendMessageViewModel.addAttachment(path) + sendMessageViewModel.addAttachments(arrayListOf(path)) } else { Log.w("$TAG Image capture was aborted") lifecycleScope.launch { @@ -892,7 +894,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { Log.i("$TAG Rich content URI [$uri] matching path is [$path]") if (path != null) { withContext(Dispatchers.Main) { - sendMessageViewModel.addAttachment(path) + sendMessageViewModel.addAttachments(arrayListOf(path)) } } } @@ -920,7 +922,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { if (files.isNotEmpty()) { Log.i("$TAG Found [${files.size}] files to share from intent") for (path in files) { - sendMessageViewModel.addAttachment(path) + sendMessageViewModel.addAttachments(arrayListOf(path)) } sharedViewModel.filesToShareFromIntent.value = arrayListOf() diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt index 101eecebec..44cae104f5 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt @@ -91,7 +91,6 @@ class FileModel private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) init { - mediaPreviewAvailable.postValue(false) updateTransferProgress(-1) formattedFileSize.postValue(FileUtils.bytesToDisplayableSize(fileSize)) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt index 904662abcb..d1575dc082 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt @@ -409,16 +409,11 @@ class MessageModel filesList.postValue(arrayListOf()) var displayableContentFound = false - var filesContentCount = 0 + var contentIndex = 0 val filesPath = arrayListOf() val contents = chatMessage.contents allFilesDownloaded = true - - val notMediaContent = contents.find { - it.isIcalendar || it.isVoiceRecording || (it.isText && !it.isFile) || it.isFileTransfer || (it.isFile && !(it.type == "video" || it.type == "image")) - } - val allContentsAreMedia = notMediaContent == null val exactly4Contents = contents.size == 4 for (content in contents) { @@ -443,7 +438,7 @@ class MessageModel } else { if (content.isFile) { Log.d("$TAG Found file content with type [${content.type}/${content.subtype}]") - filesContentCount += 1 + contentIndex += 1 checkAndRepairFilePathIfNeeded(content) @@ -462,7 +457,7 @@ class MessageModel "$TAG Found file ready to be displayed [$path] with MIME [${content.type}/${content.subtype}] for message [${chatMessage.messageId}]" ) - val wrapBefore = allContentsAreMedia && exactly4Contents && filesContentCount == 3 + val wrapBefore = exactly4Contents && contentIndex == 3 val fileSize = content.fileSize.toLong() val timestamp = content.creationTimestamp val fileModel = FileModel( @@ -488,7 +483,7 @@ class MessageModel "$TAG Found file content (not downloaded yet) with type [${content.type}/${content.subtype}] and name [${content.name}]" ) allFilesDownloaded = false - filesContentCount += 1 + contentIndex += 1 val name = content.name ?: "" val timestamp = content.creationTimestamp if (name.isNotEmpty()) { diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt index ecae462b55..6a7ac45a23 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt @@ -379,35 +379,40 @@ class SendMessageInConversationViewModel } @UiThread - fun addAttachment(file: String) { - if (attachments.value.orEmpty().size >= MAX_FILES_TO_ATTACH) { - Log.w( - "$TAG Max number of attachments [$MAX_FILES_TO_ATTACH] reached, file [$file] won't be attached" - ) - showRedToast(R.string.conversation_maximum_number_of_attachments_reached, R.drawable.warning_circle) - viewModelScope.launch { - Log.i("$TAG Deleting temporary file [$file]") - FileUtils.deleteFile(file) - } - return - } - + fun addAttachments(files: ArrayList) { val list = arrayListOf() list.addAll(attachments.value.orEmpty()) - val fileName = FileUtils.getNameFromFilePath(file) - val timestamp = System.currentTimeMillis() / 1000 - val model = FileModel(file, fileName, 0, timestamp, false, file, false) { model -> - removeAttachment(model.path) - } + for (file in files) { + if (list.size >= MAX_FILES_TO_ATTACH) { + Log.w( + "$TAG Max number of attachments [$MAX_FILES_TO_ATTACH] reached, file [$file] won't be attached" + ) + showRedToast( + R.string.conversation_maximum_number_of_attachments_reached, + R.drawable.warning_circle + ) + viewModelScope.launch { + Log.i("$TAG Deleting temporary file [$file]") + FileUtils.deleteFile(file) + } + return + } + + val fileName = FileUtils.getNameFromFilePath(file) + val timestamp = System.currentTimeMillis() / 1000 + val model = FileModel(file, fileName, 0, timestamp, false, file, false) { model -> + removeAttachment(model.path) + } - list.add(model) + list.add(model) + } attachments.value = list maxNumberOfAttachmentsReached.value = list.size >= MAX_FILES_TO_ATTACH if (list.isNotEmpty()) { isFileAttachmentsListOpen.value = true - Log.i("$TAG [${list.size}] attachment(s) added") + Log.i("$TAG [${files.size}] attachment(s) added, in total ${list.size}] file(s) are attached") } else { Log.w("$TAG No attachment to display!") } diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index aaa67004e8..d7d4da0ba1 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -330,7 +330,7 @@ private fun loadImageForChatBubble( val isVideo = FileUtils.isExtensionVideo(file) if (isImage || isVideo) { val dimen = if (grid) { - imageView.resources.getDimension(R.dimen.chat_bubble_grid_image_size).toInt() + imageView.resources.getDimension(R.dimen.chat_bubble_grid_file_size).toInt() } else { imageView.resources.getDimension(R.dimen.chat_bubble_big_image_max_size).toInt() } diff --git a/app/src/main/res/drawable/shape_squircle_file_background_top.xml b/app/src/main/res/drawable/shape_squircle_file_background_top.xml new file mode 100644 index 0000000000..f52aa40ba9 --- /dev/null +++ b/app/src/main/res/drawable/shape_squircle_file_background_top.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_squircle_file_bubble_background.xml b/app/src/main/res/drawable/shape_squircle_file_bubble_background.xml new file mode 100644 index 0000000000..733afbda3e --- /dev/null +++ b/app/src/main/res/drawable/shape_squircle_file_bubble_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_squircle_white_right.xml b/app/src/main/res/drawable/shape_squircle_file_bubble_right_background.xml similarity index 80% rename from app/src/main/res/drawable/shape_squircle_white_right.xml rename to app/src/main/res/drawable/shape_squircle_file_bubble_right_background.xml index 4015ec80ee..0f32552b6a 100644 --- a/app/src/main/res/drawable/shape_squircle_white_right.xml +++ b/app/src/main/res/drawable/shape_squircle_file_bubble_right_background.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_squircle_gray_200_r15_background.xml b/app/src/main/res/drawable/shape_squircle_gray_200_r15_background.xml new file mode 100644 index 0000000000..2b7fce2504 --- /dev/null +++ b/app/src/main/res/drawable/shape_squircle_gray_200_r15_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_bubble_content_grid_cell.xml b/app/src/main/res/layout/chat_bubble_content_grid_cell.xml index 6ab6fdfee3..4378bfc62b 100644 --- a/app/src/main/res/layout/chat_bubble_content_grid_cell.xml +++ b/app/src/main/res/layout/chat_bubble_content_grid_cell.xml @@ -1,7 +1,7 @@ + xmlns:bind="http://schemas.android.com/tools"> @@ -13,205 +13,32 @@ type="org.linphone.ui.main.chat.model.FileModel" /> - - - + app:layout_wrapBefore="@{model.flexboxLayoutWrapBefore}"> - - - - - - - - - - - - - - - - - - - - - - - - + layout="@layout/chat_bubble_file_grid_cell" + android:visibility="@{!model.isImage && !model.isVideoPreview ? View.VISIBLE : View.GONE}" + bind:model="@{model}" + bind:onLongClickListener="@{onLongClickListener}" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"/> diff --git a/app/src/main/res/layout/chat_bubble_file_grid_cell.xml b/app/src/main/res/layout/chat_bubble_file_grid_cell.xml new file mode 100644 index 0000000000..99423ebf74 --- /dev/null +++ b/app/src/main/res/layout/chat_bubble_file_grid_cell.xml @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_bubble_incoming.xml b/app/src/main/res/layout/chat_bubble_incoming.xml index b87a4e8f14..765616b4ff 100644 --- a/app/src/main/res/layout/chat_bubble_incoming.xml +++ b/app/src/main/res/layout/chat_bubble_incoming.xml @@ -175,7 +175,7 @@ android:onLongClick="@{onLongClickListener}" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:visibility="@{model.filesList.size() >= 2 || (model.filesList.size() == 1 && !model.firstFileModel.isMedia) ? View.VISIBLE : View.GONE, default=gone}" + android:visibility="@{model.filesList.size() >= 2 ? View.VISIBLE : View.GONE, default=gone}" app:alignItems="center" app:flexWrap="wrap" app:justifyContent="@{model.outgoing ? JustifyContent.FLEX_END : JustifyContent.FLEX_START}" @@ -193,6 +193,16 @@ bind:model="@{model.firstFileModel}" bind:onLongClickListener="@{onLongClickListener}"/> + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_bubble_outgoing.xml b/app/src/main/res/layout/chat_bubble_outgoing.xml index 01794927c3..24bc86e165 100644 --- a/app/src/main/res/layout/chat_bubble_outgoing.xml +++ b/app/src/main/res/layout/chat_bubble_outgoing.xml @@ -147,7 +147,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:onLongClick="@{onLongClickListener}" - android:visibility="@{model.filesList.size() >= 2 || (model.filesList.size() == 1 && !model.firstFileModel.isMedia) ? View.VISIBLE : View.GONE, default=gone}" + android:visibility="@{model.filesList.size() >= 2 ? View.VISIBLE : View.GONE, default=gone}" app:alignItems="center" app:flexWrap="wrap" app:justifyContent="@{model.outgoing ? JustifyContent.FLEX_END : JustifyContent.FLEX_START}" @@ -165,6 +165,16 @@ bind:model="@{model.firstFileModel}" bind:onLongClickListener="@{onLongClickListener}"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_bubble_single_media_content.xml b/app/src/main/res/layout/chat_bubble_single_media_content.xml index a9ee9d23fc..6fb6aa1960 100644 --- a/app/src/main/res/layout/chat_bubble_single_media_content.xml +++ b/app/src/main/res/layout/chat_bubble_single_media_content.xml @@ -25,8 +25,8 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:bind="http://schemas.android.com/tools"> - - - - + android:layout_height="wrap_content"> - - - - - - - - + app:layout_constraintTop_toTopOf="parent"/> - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/chat_media_content_grid_cell.xml b/app/src/main/res/layout/chat_media_content_grid_cell.xml index 8c028699a7..80b82a7d3f 100644 --- a/app/src/main/res/layout/chat_media_content_grid_cell.xml +++ b/app/src/main/res/layout/chat_media_content_grid_cell.xml @@ -1,6 +1,7 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:bind="http://schemas.android.com/tools"> @@ -47,37 +48,6 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent"/> - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 5acd61d935..8d99aa567e 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -83,9 +83,9 @@ 2dp 8dp 25dp - 87dp 175dp - 178dp + 87dp + 178dp 271dp 230dp 271dp @@ -94,6 +94,8 @@ 5dp 15dp 24dp + 5dp + 10dp 300dp 300dp From 21398c7b377b0e90aa3eb65e0fda20eab380a589 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 20 May 2025 13:48:19 +0200 Subject: [PATCH 154/593] Added sliding button to answer/decline incoming call if device screen is locked --- .../ui/call/fragment/IncomingCallFragment.kt | 71 ++++++++ .../ui/call/viewmodel/CurrentCallViewModel.kt | 78 +++++--- app/src/main/res/drawable/arrow_green.xml | 21 +++ app/src/main/res/drawable/arrow_red.xml | 21 +++ .../main/res/layout/call_incoming_actions.xml | 166 +++++++++++++++--- app/src/main/res/values/dimen.xml | 1 + 6 files changed, 309 insertions(+), 49 deletions(-) create mode 100644 app/src/main/res/drawable/arrow_green.xml create mode 100644 app/src/main/res/drawable/arrow_red.xml diff --git a/app/src/main/java/org/linphone/ui/call/fragment/IncomingCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/IncomingCallFragment.kt index ea5f9ad578..e9b9a2691d 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/IncomingCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/IncomingCallFragment.kt @@ -19,16 +19,23 @@ */ package org.linphone.ui.call.fragment +import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import androidx.annotation.UiThread import androidx.lifecycle.ViewModelProvider import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R import org.linphone.core.tools.Log import org.linphone.databinding.CallIncomingFragmentBinding import org.linphone.ui.call.viewmodel.CurrentCallViewModel +import org.linphone.utils.AppUtils +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min @UiThread class IncomingCallFragment : GenericCallFragment() { @@ -40,6 +47,66 @@ class IncomingCallFragment : GenericCallFragment() { private lateinit var callViewModel: CurrentCallViewModel + private val marginSize = AppUtils.getDimension(R.dimen.sliding_accept_decline_call_margin) + private val areaSize = AppUtils.getDimension(R.dimen.call_button_size) + marginSize + private var initialX = 0f + private var slidingButtonX = 0f + private val slidingButtonTouchListener = View.OnTouchListener { view, event -> + val width = binding.bottomBar.root.width.toFloat() + val aboveAnswer = view.x + view.width > width - areaSize + val aboveDecline = view.x < areaSize + + when (event.action) { + MotionEvent.ACTION_DOWN -> { + if (initialX == 0f) { + initialX = view.x + } + slidingButtonX = view.x - event.rawX + true + } + MotionEvent.ACTION_UP -> { + if (aboveAnswer) { + // Accept + callViewModel.answer() + } else if (aboveDecline) { + // Decline + callViewModel.hangUp() + } else { + // Animate going back to initial position + view.animate() + .x(initialX) + .setDuration(500) + .start() + } + true + } + MotionEvent.ACTION_MOVE -> { + callViewModel.slidingButtonAboveAnswer.value = aboveAnswer + callViewModel.slidingButtonAboveDecline.value = aboveDecline + + val offset = view.x - initialX + val percent = abs(offset) / (width / 2) + if (offset > 0) { + callViewModel.answerAlpha.value = 1f + callViewModel.declineAlpha.value = 1f - percent + } else if (offset < 0) { + callViewModel.answerAlpha.value = 1f - percent + callViewModel.declineAlpha.value = 1f + } + + view.animate() + .x(min(max(marginSize, event.rawX + slidingButtonX), width - view.width - marginSize)) + .setDuration(0) + .start() + true + } + else -> { + view.performClick() + false + } + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -49,6 +116,7 @@ class IncomingCallFragment : GenericCallFragment() { return binding.root } + @SuppressLint("ClickableViewAccessibility") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -68,11 +136,14 @@ class IncomingCallFragment : GenericCallFragment() { } } } + + binding.bottomBar.slidingButton.setOnTouchListener(slidingButtonTouchListener) } override fun onResume() { super.onResume() + callViewModel.refreshKeyguardLockedStatus() coreContext.notificationsManager.setIncomingCallFragmentCurrentlyDisplayed(true) } diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 22a2ccab2b..9e40010b11 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -20,6 +20,8 @@ package org.linphone.ui.call.viewmodel import android.Manifest +import android.app.KeyguardManager +import android.content.Context import android.content.pm.PackageManager import androidx.annotation.AnyThread import androidx.annotation.UiThread @@ -258,6 +260,18 @@ class CurrentCallViewModel MutableLiveData>() } + // Sliding answer/decline button + + val isScreenLocked = MutableLiveData() + + val slidingButtonAboveAnswer = MutableLiveData() + + val slidingButtonAboveDecline = MutableLiveData() + + val answerAlpha = MutableLiveData() + + val declineAlpha = MutableLiveData() + lateinit var currentCall: Call private val contactsListener = object : ContactsListener { @@ -517,32 +531,6 @@ class CurrentCallViewModel } } - @WorkerThread - private fun updateProximitySensor() { - if (::currentCall.isInitialized) { - val callState = currentCall.state - if (LinphoneUtils.isCallIncoming(callState)) { - proximitySensorEnabled.postValue(false) - } else if (LinphoneUtils.isCallOutgoing(callState)) { - val videoEnabled = currentCall.params.isVideoEnabled - proximitySensorEnabled.postValue(!videoEnabled) - } else { - if (isSendingVideo.value == true || isReceivingVideo.value == true) { - proximitySensorEnabled.postValue(false) - } else { - val outputAudioDevice = currentCall.outputAudioDevice ?: coreContext.core.outputAudioDevice - if (outputAudioDevice != null && outputAudioDevice.type == AudioDevice.Type.Earpiece) { - proximitySensorEnabled.postValue(true) - } else { - proximitySensorEnabled.postValue(false) - } - } - } - } else { - proximitySensorEnabled.postValue(false) - } - } - init { fullScreenMode.value = false operationInProgress.value = false @@ -550,6 +538,10 @@ class CurrentCallViewModel videoUpdateInProgress.value = false microphoneRecordingVolume.value = 0f + refreshKeyguardLockedStatus() + answerAlpha.value = 1f + declineAlpha.value = 1f + coreContext.postOnCoreThread { core -> hideSipAddresses.postValue(corePreferences.hideSipAddresses) coreContext.contactsManager.addListener(contactsListener) @@ -613,6 +605,14 @@ class CurrentCallViewModel } } + @UiThread + fun refreshKeyguardLockedStatus() { + val keyguardManager = coreContext.context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager + val secure = keyguardManager.isKeyguardLocked + isScreenLocked.value = secure + Log.i("$TAG Device is [${if (secure) "locked" else "unlocked"}]") + } + @UiThread fun answer() { coreContext.postOnCoreThread { core -> @@ -1542,4 +1542,30 @@ class CurrentCallViewModel if (volume > VU_METER_MAX) return 1f return (volume - VU_METER_MIN) / (VU_METER_MAX - VU_METER_MIN) } + + @WorkerThread + private fun updateProximitySensor() { + if (::currentCall.isInitialized) { + val callState = currentCall.state + if (LinphoneUtils.isCallIncoming(callState)) { + proximitySensorEnabled.postValue(false) + } else if (LinphoneUtils.isCallOutgoing(callState)) { + val videoEnabled = currentCall.params.isVideoEnabled + proximitySensorEnabled.postValue(!videoEnabled) + } else { + if (isSendingVideo.value == true || isReceivingVideo.value == true) { + proximitySensorEnabled.postValue(false) + } else { + val outputAudioDevice = currentCall.outputAudioDevice ?: coreContext.core.outputAudioDevice + if (outputAudioDevice != null && outputAudioDevice.type == AudioDevice.Type.Earpiece) { + proximitySensorEnabled.postValue(true) + } else { + proximitySensorEnabled.postValue(false) + } + } + } + } else { + proximitySensorEnabled.postValue(false) + } + } } diff --git a/app/src/main/res/drawable/arrow_green.xml b/app/src/main/res/drawable/arrow_green.xml new file mode 100644 index 0000000000..27f31c09c8 --- /dev/null +++ b/app/src/main/res/drawable/arrow_green.xml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/arrow_red.xml b/app/src/main/res/drawable/arrow_red.xml new file mode 100644 index 0000000000..6fd056270e --- /dev/null +++ b/app/src/main/res/drawable/arrow_red.xml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/call_incoming_actions.xml b/app/src/main/res/layout/call_incoming_actions.xml index 015b0a7b35..8346e38ae9 100644 --- a/app/src/main/res/layout/call_incoming_actions.xml +++ b/app/src/main/res/layout/call_incoming_actions.xml @@ -14,6 +14,146 @@ android:layout_height="@dimen/call_main_actions_menu_height" android:background="@drawable/shape_call_bottom_sheet_background"> + + + + + + + + + + + + + + + + + + + + + + - - diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 8d99aa567e..7e68dc955a 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -65,6 +65,7 @@ 20dp 5dp 55dp + 10dp 15dp 65dp 30dp From b6279b03c074b8c71dedd5ffffb2217e8f05a577 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 20 May 2025 16:33:22 +0200 Subject: [PATCH 155/593] Make sure that files grid in chat bubble is using at most 3 columns --- .../org/linphone/ui/main/chat/model/MessageModel.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt index d1575dc082..298d62c80d 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt @@ -436,6 +436,11 @@ class MessageModel displayableContentFound = true } else { + val wrapBefore = if (exactly4Contents) { + contentIndex == 2 // To have a 2x2 grid + } else { + contentIndex % 3 == 0 // To have at most 3 columns + } if (content.isFile) { Log.d("$TAG Found file content with type [${content.type}/${content.subtype}]") contentIndex += 1 @@ -456,8 +461,6 @@ class MessageModel Log.d( "$TAG Found file ready to be displayed [$path] with MIME [${content.type}/${content.subtype}] for message [${chatMessage.messageId}]" ) - - val wrapBefore = exactly4Contents && contentIndex == 3 val fileSize = content.fileSize.toLong() val timestamp = content.creationTimestamp val fileModel = FileModel( @@ -496,7 +499,8 @@ class MessageModel timestamp, isFileEncrypted, path, - chatMessage.isEphemeral + chatMessage.isEphemeral, + flexboxLayoutWrapBefore = wrapBefore ) { model -> onContentClicked?.invoke(model) } @@ -509,7 +513,8 @@ class MessageModel isFileEncrypted, name, chatMessage.isEphemeral, - isWaitingToBeDownloaded = true + isWaitingToBeDownloaded = true, + flexboxLayoutWrapBefore = wrapBefore ) { model -> downloadContent(model, content) } From c0f67d01fe729ab1d731ea6c8e0b1ca807e26205 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 21 May 2025 10:11:18 +0200 Subject: [PATCH 156/593] Prevent crash in MediaViewerFragment if media player wasn't initialized --- .../ui/fileviewer/fragment/MediaViewerFragment.kt | 4 ++-- .../linphone/ui/fileviewer/viewmodel/MediaViewModel.kt | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/fileviewer/fragment/MediaViewerFragment.kt b/app/src/main/java/org/linphone/ui/fileviewer/fragment/MediaViewerFragment.kt index 9dcc8ba3f9..d3e8b3dd46 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/fragment/MediaViewerFragment.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/fragment/MediaViewerFragment.kt @@ -109,7 +109,7 @@ class MediaViewerFragment : GenericMainFragment() { val textureView = binding.videoPlayer if (textureView.isAvailable) { Log.i("$TAG Surface created, setting display in mediaPlayer") - viewModel.mediaPlayer.setSurface((Surface(textureView.surfaceTexture))) + viewModel.setMediaPlayerSurface((Surface(textureView.surfaceTexture))) } else { Log.i("$TAG Surface not available yet, setting listener") textureView.surfaceTextureListener = object : SurfaceTextureListener { @@ -119,7 +119,7 @@ class MediaViewerFragment : GenericMainFragment() { p2: Int ) { Log.i("$TAG Surface available, setting display in mediaPlayer") - viewModel.mediaPlayer.setSurface(Surface(surfaceTexture)) + viewModel.setMediaPlayerSurface(Surface(surfaceTexture)) } override fun onSurfaceTextureSizeChanged( diff --git a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/MediaViewModel.kt b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/MediaViewModel.kt index eecc3ffe08..39f524b9d9 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/MediaViewModel.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/MediaViewModel.kt @@ -21,6 +21,7 @@ package org.linphone.ui.fileviewer.viewmodel import android.media.AudioAttributes import android.media.MediaPlayer +import android.view.Surface import androidx.annotation.UiThread import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope @@ -225,4 +226,11 @@ class MediaViewModel updatePositionJob?.cancel() updatePositionJob = null } + + @UiThread + fun setMediaPlayerSurface(surface: Surface) { + if (::mediaPlayer.isInitialized) { + mediaPlayer.setSurface(surface) + } + } } From cea2d49778fe270e230cbcaedef038b782ee5859 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 21 May 2025 14:25:03 +0200 Subject: [PATCH 157/593] Trying to workaround hearing aids issue in Telecom Manager --- .../telecom/TelecomCallControlCallback.kt | 48 ++++++++++++++----- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt index d7ac38e0ae..9590fbb43e 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt @@ -53,6 +53,7 @@ class TelecomCallControlCallback( private var availableEndpoints: List = arrayListOf() private var currentEndpoint = CallEndpointCompat.TYPE_UNKNOWN private var endpointUpdateRequestFromLinphone: Boolean = false + private var latestLinphoneRequestedEndpoint: CallEndpointCompat? = null private val callListener = object : CallListenerStub() { @WorkerThread @@ -138,18 +139,27 @@ class TelecomCallControlCallback( }.launchIn(scope) callControl.currentCallEndpoint.onEach { endpoint -> - val type = endpoint.type - currentEndpoint = type + var newEndpointToUse = endpoint if (endpointUpdateRequestFromLinphone) { - Log.i("$TAG Linphone requests to use [${endpoint.name}] audio endpoint with type [$type]") + Log.i("$TAG Linphone requests to use [${endpoint.name}] audio endpoint with type [${endpointTypeToString(endpoint.type)}]") } else { - Log.i("$TAG Android requests us to use [${endpoint.name}] audio endpoint with type [$type]") + Log.i("$TAG Android requests us to use [${endpoint.name}] audio endpoint with type [${endpointTypeToString(endpoint.type)}]") + } + + val requestedEndpoint = latestLinphoneRequestedEndpoint + if (endpointUpdateRequestFromLinphone && requestedEndpoint != null && requestedEndpoint != endpoint) { + Log.w("$TAG WARNING: Linphone requested endpoint [${requestedEndpoint.name}] but Telecom Manager notified endpoint [${endpoint.name}], trying to use the one we requested anyway") + newEndpointToUse = requestedEndpoint } + val type = newEndpointToUse.type + currentEndpoint = type if (!endpointUpdateRequestFromLinphone && !coreContext.isConnectedToAndroidAuto && (type == CallEndpointCompat.Companion.TYPE_EARPIECE || type == CallEndpointCompat.Companion.TYPE_SPEAKER)) { - Log.w("$TAG Device isn't connected to Android Auto, do not follow system request to change audio endpoint to either earpiece or speaker") + endpointUpdateRequestFromLinphone = false + Log.w("$TAG Device isn't connected to Android Auto, do not follow system request to change audio endpoint to [${newEndpointToUse.name}] with type [${endpointTypeToString(type)}]") return@onEach } + endpointUpdateRequestFromLinphone = false // Change audio route in SDK, this way the usual listener will trigger // and we'll be able to update the UI accordingly @@ -180,7 +190,6 @@ class TelecomCallControlCallback( } } } - endpointUpdateRequestFromLinphone = false }.launchIn(scope) callControl.isMuted.onEach { muted -> @@ -210,13 +219,13 @@ class TelecomCallControlCallback( } fun applyAudioRouteToCallWithId(routes: List): Boolean { - endpointUpdateRequestFromLinphone = true Log.i("$TAG Looking for audio endpoint with type [${routes.first()}]") var wiredHeadsetFound = false + var skippedBecauseAlreadyInUse = false for (endpoint in availableEndpoints) { Log.i( - "$TAG Found audio endpoint [${endpoint.name}] with type [${endpoint.type}]" + "$TAG Found audio endpoint [${endpoint.name}] with type [${endpointTypeToString(endpoint.type)}]" ) val matches = when (endpoint.type) { CallEndpointCompat.Companion.TYPE_EARPIECE -> { @@ -237,21 +246,24 @@ class TelecomCallControlCallback( if (matches != null) { Log.i( - "$TAG Found matching audio endpoint [${endpoint.name}], trying to use it" + "$TAG Found matching audio endpoint [${endpoint.name}] with type [${endpointTypeToString(endpoint.type)}], trying to use it" ) if (currentEndpoint == endpoint.type) { Log.w("$TAG Endpoint already in use, skipping") + skippedBecauseAlreadyInUse = true continue } scope.launch { - Log.i("$TAG Requesting audio endpoint change with [${endpoint.name}]") + Log.i("$TAG Requesting audio endpoint change to [${endpoint.name}] with type [${endpointTypeToString(endpoint.type)}]") + endpointUpdateRequestFromLinphone = true + latestLinphoneRequestedEndpoint = endpoint var result: CallControlResult = callControl.requestEndpointChange(endpoint) var attempts = 1 while (result is CallControlResult.Error && attempts <= 10) { delay(100) Log.i( - "$TAG Previous attempt failed [$result], requesting again audio endpoint change with [${endpoint.name}]" + "$TAG Previous attempt failed [$result], requesting again audio endpoint change to [${endpoint.name}] with type [${endpointTypeToString(endpoint.type)}]" ) result = callControl.requestEndpointChange(endpoint) attempts += 1 @@ -273,6 +285,8 @@ class TelecomCallControlCallback( if (routes.size == 1 && routes[0] == AudioDevice.Type.Earpiece && wiredHeadsetFound) { Log.e("$TAG User asked for earpiece but endpoint doesn't exists!") + } else if (skippedBecauseAlreadyInUse) { + Log.w("$TAG This endpoint was already in use (according to Telecom Manager), force changing the device in Linphone just in case") } else { Log.e("$TAG No matching endpoint found") } @@ -364,4 +378,16 @@ class TelecomCallControlCallback( else -> "UNEXPECTED: $cause" } } + + private fun endpointTypeToString(type: Int): String { + return when (type) { + CallEndpointCompat.Companion.TYPE_UNKNOWN -> "UNKNOWN" + CallEndpointCompat.Companion.TYPE_EARPIECE -> "EARPIECE" + CallEndpointCompat.Companion.TYPE_BLUETOOTH -> "BLUETOOTH" + CallEndpointCompat.Companion.TYPE_WIRED_HEADSET -> "WIRED HEADSET" + CallEndpointCompat.Companion.TYPE_SPEAKER -> "SPEAKER" + CallEndpointCompat.Companion.TYPE_STREAMING -> "STREAMING" + else -> "UNEXPECTED: $type" + } + } } From 27e59a5f8b92b93b16dc5eab2120cb9246fe0c25 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 21 May 2025 13:44:27 +0200 Subject: [PATCH 158/593] Using utils method to check whether call has video enabled or not --- .../java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 9e40010b11..c44a6f74ff 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -357,7 +357,7 @@ class CurrentCallViewModel endCall(call) } } else { - val videoEnabled = call.currentParams.isVideoEnabled + val videoEnabled = LinphoneUtils.isVideoEnabled(call) if (videoEnabled && isVideoEnabled.value == false) { if (isBluetoothEnabled.value == true || isHeadsetEnabled.value == true) { Log.i( From 5256ee79c6f298bc2aefd4d0ee44390473991146 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 22 May 2025 10:48:48 +0200 Subject: [PATCH 159/593] Use gray background for file preview in attachment area to increase contrast, improved remove file from attachments icon size and position --- .../shape_squircle_file_bubble_gray_background.xml | 5 +++++ .../main/res/layout/chat_bubble_file_grid_cell.xml | 5 ++++- .../chat_conversation_attachments_area_cell.xml | 12 +++++++----- 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 app/src/main/res/drawable/shape_squircle_file_bubble_gray_background.xml diff --git a/app/src/main/res/drawable/shape_squircle_file_bubble_gray_background.xml b/app/src/main/res/drawable/shape_squircle_file_bubble_gray_background.xml new file mode 100644 index 0000000000..ea2766b996 --- /dev/null +++ b/app/src/main/res/drawable/shape_squircle_file_bubble_gray_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_bubble_file_grid_cell.xml b/app/src/main/res/layout/chat_bubble_file_grid_cell.xml index 99423ebf74..24b67390ae 100644 --- a/app/src/main/res/layout/chat_bubble_file_grid_cell.xml +++ b/app/src/main/res/layout/chat_bubble_file_grid_cell.xml @@ -8,6 +8,9 @@ + @@ -22,7 +25,7 @@ android:id="@+id/background" android:layout_width="@dimen/chat_bubble_grid_file_size" android:layout_height="@dimen/chat_bubble_grid_file_size" - android:background="@drawable/shape_squircle_file_bubble_background" + android:background="@{useGrayBackground ? @drawable/shape_squircle_file_bubble_gray_background : @drawable/shape_squircle_file_bubble_background, default=@drawable/shape_squircle_file_bubble_background}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent"/> diff --git a/app/src/main/res/layout/chat_conversation_attachments_area_cell.xml b/app/src/main/res/layout/chat_conversation_attachments_area_cell.xml index 97124ef66d..c713540586 100644 --- a/app/src/main/res/layout/chat_conversation_attachments_area_cell.xml +++ b/app/src/main/res/layout/chat_conversation_attachments_area_cell.xml @@ -12,7 +12,8 @@ + android:layout_height="wrap_content" + android:layout_margin="2dp"> Date: Thu, 22 May 2025 14:45:31 +0200 Subject: [PATCH 160/593] Fixed sent files size missing --- .../linphone/ui/main/chat/model/FileModel.kt | 7 ++++- .../ui/main/chat/model/MessageModel.kt | 29 ++++++++++++------- .../SendMessageInConversationViewModel.kt | 3 +- .../main/java/org/linphone/utils/FileUtils.kt | 11 +++++++ 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt index 44cae104f5..1b4c2d315c 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt @@ -92,7 +92,7 @@ class FileModel init { updateTransferProgress(-1) - formattedFileSize.postValue(FileUtils.bytesToDisplayableSize(fileSize)) + computeFileSize(fileSize) if (!isWaitingToBeDownloaded) { val extension = FileUtils.getExtensionFromFileName(path) @@ -141,6 +141,11 @@ class FileModel } } + @AnyThread + fun computeFileSize(fileSize: Long) { + formattedFileSize.postValue(FileUtils.bytesToDisplayableSize(fileSize)) + } + @AnyThread fun updateTransferProgress(percent: Int) { transferProgress.postValue(percent) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt index 298d62c80d..fe6968ac50 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt @@ -217,13 +217,13 @@ class MessageModel transferringFileModel = null if (!allFilesDownloaded) { computeContentsList() - } - - for (content in message.contents) { - if (content.isVoiceRecording) { - Log.i("$TAG File transfer done, updating voice record info") - computeVoiceRecordContent(content) - break + } else { + for (content in message.contents) { + if (content.isVoiceRecording) { + Log.i("$TAG File transfer done, updating voice record info") + computeVoiceRecordContent(content) + break + } } } } @@ -406,7 +406,7 @@ class MessageModel private fun computeContentsList() { Log.d("$TAG Computing message contents list") text.postValue(Spannable.Factory.getInstance().newSpannable("")) - filesList.postValue(arrayListOf()) + filesList.value.orEmpty().forEach(FileModel::destroy) var displayableContentFound = false var contentIndex = 0 @@ -461,7 +461,11 @@ class MessageModel Log.d( "$TAG Found file ready to be displayed [$path] with MIME [${content.type}/${content.subtype}] for message [${chatMessage.messageId}]" ) - val fileSize = content.fileSize.toLong() + val fileSize = if (content.fileSize.toLong() > 0) { + content.fileSize.toLong() + } else { + FileUtils.getFileSize(path) + } val timestamp = content.creationTimestamp val fileModel = FileModel( path, @@ -492,10 +496,15 @@ class MessageModel if (name.isNotEmpty()) { val fileModel = if (isOutgoing && chatMessage.isFileTransferInProgress) { val path = content.filePath.orEmpty() + val fileSize = if (content.fileSize.toLong() > 0) { + content.fileSize.toLong() + } else { + FileUtils.getFileSize(path) + } FileModel( path, name, - content.fileSize.toLong(), + fileSize, timestamp, isFileEncrypted, path, diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt index 6a7ac45a23..ac70255471 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt @@ -401,7 +401,8 @@ class SendMessageInConversationViewModel val fileName = FileUtils.getNameFromFilePath(file) val timestamp = System.currentTimeMillis() / 1000 - val model = FileModel(file, fileName, 0, timestamp, false, file, false) { model -> + val size = FileUtils.getFileSize(file) + val model = FileModel(file, fileName, size, timestamp, false, file, false) { model -> removeAttachment(model.path) } diff --git a/app/src/main/java/org/linphone/utils/FileUtils.kt b/app/src/main/java/org/linphone/utils/FileUtils.kt index 32770117f9..8500de8e74 100644 --- a/app/src/main/java/org/linphone/utils/FileUtils.kt +++ b/app/src/main/java/org/linphone/utils/FileUtils.kt @@ -68,6 +68,17 @@ class FileUtils { return Formatter.formatShortFileSize(coreContext.context, bytes) } + @AnyThread + fun getFileSize(filePath: String): Long { + try { + val file = File(filePath) + return file.length() + } catch (e: Exception) { + Log.e("$TAG Failed to get file [$filePath] size: $e") + } + return 0L + } + @AnyThread fun isExtensionImage(path: String): Boolean { val extension = getExtensionFromFileName(path) From 17ce34aba7ec056f76c59bc8026c35a6ebbe7c5b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 23 May 2025 15:04:55 +0200 Subject: [PATCH 161/593] Updated changelog & version code/name from release/6.0 branch --- CHANGELOG.md | 17 +++++++++++++++++ app/build.gradle.kts | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c962dc70d..48d6c6a4cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,23 @@ Group changes to describe their impact on the project, as follows: ### Changed - Reworked some settings (moved calls related ones from advanced settings to advanced calls settings) +## [6.0.8] - 2025-05-23 + +### Added +- Ukrainian & simplified Chinese translations from Weblate +- Sliding answer/decline button in incoming call fragment if device is locked (will help prevent calls from being unintentionally picked up or hung up while the device is being removed from a pocket) + +### Changed +- Show files with square design when more than one (as it is for media files) +- Outgoing chat bubbles will now display the sent file size (as it is for received messages) + +### Fixed +- Fixed issue with bluetooth hearing aids +- Fixed audio call being answered on speakerphone +- Fixed events related to joined/left conversation being briefly visible sometimes for 1-1 conversations +- Fixed files/media grid in chat bubble using more than 3 columns in landscape +- Fixed logs upload server URL setting + ## [6.0.7] - 2025-05-16 ### Added diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a39ed72b0c..d9ad569f7e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,7 +32,7 @@ if (firebaseCloudMessagingAvailable) { } var gitBranch = ByteArrayOutputStream() -var gitVersion = "6.0.7" +var gitVersion = "6.0.8" task("getGitVersion") { val gitVersionStream = ByteArrayOutputStream() @@ -100,8 +100,8 @@ android { applicationId = packageName minSdk = 28 targetSdk = 36 - versionCode = 600007 // 6.00.007 - versionName = "6.0.7" + versionCode = 600008 // 6.00.008 + versionName = "6.0.8" manifestPlaceholders["appAuthRedirectScheme"] = packageName From def52f69ad40a14675c084c0c3be2d93a28dd1f7 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 29 May 2025 11:40:42 +0200 Subject: [PATCH 162/593] Only refresh conversation list cell when a message is deleted, prevents blinking --- .../ConversationEphemeralLifetimeFragment.kt | 2 +- .../ui/main/chat/fragment/ConversationFragment.kt | 11 +++++------ .../main/chat/fragment/ConversationInfoFragment.kt | 8 ++++---- .../chat/fragment/ConversationsListFragment.kt | 11 +++++++---- .../ui/main/chat/model/ConversationModel.kt | 14 ++++++++++++++ .../ui/main/viewmodel/SharedMainViewModel.kt | 14 +++++++------- 6 files changed, 38 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationEphemeralLifetimeFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationEphemeralLifetimeFragment.kt index 96332c9c5a..b29ee0a230 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationEphemeralLifetimeFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationEphemeralLifetimeFragment.kt @@ -82,7 +82,7 @@ class ConversationEphemeralLifetimeFragment : SlidingPaneChildFragment() { } override fun onPause() { - sharedViewModel.newChatMessageEphemeralLifetimeToSet.value = Event( + sharedViewModel.newChatMessageEphemeralLifetimeToSetEvent.value = Event( viewModel.currentlySelectedValue.value ?: 0L ) super.onPause() diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 0e082fd0b6..1fe062ec86 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -94,7 +94,6 @@ import org.linphone.utils.hideKeyboard import org.linphone.utils.setKeyboardInsetListener import org.linphone.utils.showKeyboard import androidx.core.net.toUri -import androidx.lifecycle.observe @UiThread open class ConversationFragment : SlidingPaneChildFragment() { @@ -819,7 +818,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { val message = getString(R.string.conversation_message_deleted_toast) val icon = R.drawable.trash_simple (requireActivity() as GenericActivity).showGreenToast(message, icon) - sharedViewModel.forceRefreshConversations.value = Event(true) + sharedViewModel.updateConversationLastMessageEvent.value = Event(viewModel.conversationId) } } @@ -929,7 +928,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { } } - sharedViewModel.forceRefreshConversationInfo.observe(viewLifecycleOwner) { + sharedViewModel.forceRefreshConversationInfoEvent.observe(viewLifecycleOwner) { it.consume { Log.i("$TAG Force refreshing conversation info") viewModel.refresh() @@ -943,7 +942,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { } } - sharedViewModel.newChatMessageEphemeralLifetimeToSet.observe(viewLifecycleOwner) { + sharedViewModel.newChatMessageEphemeralLifetimeToSetEvent.observe(viewLifecycleOwner) { it.consume { ephemeralLifetime -> Log.i( "$TAG Setting [$ephemeralLifetime] as new ephemeral lifetime for messages" @@ -1190,14 +1189,14 @@ open class ConversationFragment : SlidingPaneChildFragment() { Log.i("$TAG Muting conversation") viewModel.mute() popupWindow.dismiss() - sharedViewModel.forceRefreshDisplayedConversation.value = Event(true) + sharedViewModel.forceRefreshDisplayedConversationEvent.value = Event(true) } popupView.setUnmuteClickListener { Log.i("$TAG Un-muting conversation") viewModel.unMute() popupWindow.dismiss() - sharedViewModel.forceRefreshDisplayedConversation.value = Event(true) + sharedViewModel.forceRefreshDisplayedConversationEvent.value = Event(true) } popupView.setConfigureEphemeralMessagesClickListener { diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt index ba158ae8fe..e804acabd5 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt @@ -136,7 +136,7 @@ class ConversationInfoFragment : SlidingPaneChildFragment() { viewModel.groupLeftEvent.observe(viewLifecycleOwner) { it.consume { Log.i("$TAG Group has been left, leaving conversation info...") - sharedViewModel.forceRefreshConversationInfo.value = Event(true) + sharedViewModel.forceRefreshConversationInfoEvent.value = Event(true) goBack() val message = getString(R.string.conversation_group_left_toast) (requireActivity() as GenericActivity).showGreenToast( @@ -149,7 +149,7 @@ class ConversationInfoFragment : SlidingPaneChildFragment() { viewModel.historyDeletedEvent.observe(viewLifecycleOwner) { it.consume { Log.i("$TAG History has been deleted, leaving conversation info...") - sharedViewModel.forceRefreshConversations.value = Event(true) + sharedViewModel.updateConversationLastMessageEvent.value = Event(viewModel.conversationId) sharedViewModel.forceRefreshConversationEvents.value = Event(true) goBack() val message = getString(R.string.conversation_info_history_deleted_toast) @@ -180,7 +180,7 @@ class ConversationInfoFragment : SlidingPaneChildFragment() { viewModel.infoChangedEvent.observe(viewLifecycleOwner) { it.consume { - sharedViewModel.forceRefreshConversationInfo.postValue(Event(true)) + sharedViewModel.forceRefreshConversationInfoEvent.postValue(Event(true)) } } @@ -197,7 +197,7 @@ class ConversationInfoFragment : SlidingPaneChildFragment() { } } - sharedViewModel.newChatMessageEphemeralLifetimeToSet.observe(viewLifecycleOwner) { + sharedViewModel.newChatMessageEphemeralLifetimeToSetEvent.observe(viewLifecycleOwner) { it.consume { ephemeralLifetime -> Log.i( "$TAG Setting [$ephemeralLifetime] as new ephemeral lifetime for messages" diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt index 862137b472..8bbd9548cc 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt @@ -274,13 +274,16 @@ class ConversationsListFragment : AbstractMainFragment() { } } - sharedViewModel.forceRefreshConversations.observe(viewLifecycleOwner) { - it.consume { - listViewModel.filter() + sharedViewModel.updateConversationLastMessageEvent.observe(viewLifecycleOwner) { + it.consume { conversationId -> + val model = listViewModel.conversations.value.orEmpty().find { + it.id == conversationId + } + model?.updateLastMessageInfo() } } - sharedViewModel.forceRefreshDisplayedConversation.observe(viewLifecycleOwner) { + sharedViewModel.forceRefreshDisplayedConversationEvent.observe(viewLifecycleOwner) { it.consume { val displayChatRoom = sharedViewModel.displayedChatRoom if (displayChatRoom != null) { diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt index 0917992731..6392b6714d 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt @@ -20,8 +20,10 @@ package org.linphone.ui.main.chat.model import android.text.Spannable +import android.text.SpannableStringBuilder import androidx.annotation.UiThread import androidx.annotation.WorkerThread +import androidx.core.text.toSpannable import androidx.lifecycle.MutableLiveData import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R @@ -272,6 +274,13 @@ class ConversationModel } } + @UiThread + fun updateLastMessageInfo() { + coreContext.postOnCoreThread { + updateLastMessage() + } + } + @WorkerThread private fun updateLastMessageStatus(message: ChatMessage) { val isOutgoing = message.isOutgoing @@ -341,6 +350,11 @@ class ConversationModel lastMessage = message } } else { + lastMessage = null + lastMessageTextSender.postValue("") + lastMessageContentIcon.postValue(0) + lastMessageText.postValue(SpannableStringBuilder("").toSpannable()) + isLastMessageOutgoing.postValue(false) Log.w("$TAG No last message to display for conversation [$id]") } } diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt index 61aa32472f..952a0b45b8 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/SharedMainViewModel.kt @@ -142,15 +142,11 @@ class SharedMainViewModel MutableLiveData>() } - val forceRefreshDisplayedConversation: MutableLiveData> by lazy { + val forceRefreshDisplayedConversationEvent: MutableLiveData> by lazy { MutableLiveData>() } - val forceRefreshConversations: MutableLiveData> by lazy { - MutableLiveData>() - } - - val forceRefreshConversationInfo: MutableLiveData> by lazy { + val forceRefreshConversationInfoEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -158,10 +154,14 @@ class SharedMainViewModel MutableLiveData>() } - val newChatMessageEphemeralLifetimeToSet: MutableLiveData> by lazy { + val newChatMessageEphemeralLifetimeToSetEvent: MutableLiveData> by lazy { MutableLiveData>() } + val updateConversationLastMessageEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + val updateUnreadMessageCountForCurrentConversationEvent: MutableLiveData> by lazy { MutableLiveData>() } From 4cb83980ba885b8eac05ea7cee4263bb527a5597 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 30 May 2025 09:42:33 +0200 Subject: [PATCH 163/593] Bumped AGP --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d75f064617..4005b23b31 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.10.0" +agp = "8.10.1" kotlin = "2.0.21" gmsGoogleServices = "4.4.2" firebaseCrashlytics = "3.0.3" From d212b7b06e28b22fcf744da66c4d8f5bd9453b7d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 30 May 2025 10:09:56 +0200 Subject: [PATCH 164/593] Added setting to hide contacts without SIP address nor phone number --- .../java/org/linphone/core/CorePreferences.kt | 7 +++++ .../viewmodel/ContactsListViewModel.kt | 6 ++++ .../settings/viewmodel/SettingsViewModel.kt | 11 +++++++ app/src/main/res/layout/settings_contacts.xml | 29 ++++++++++++++++++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 54 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index b91feaefd5..5d5a8f0022 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -224,6 +224,13 @@ class CorePreferences config.setBool("ui", "sort_contacts_by_first_name", value) } + @get:WorkerThread + var hideContactsWithoutPhoneNumberOrSipAddress: Boolean + get() = config.getBool("ui", "hide_contacts_without_phone_number_or_sip_address", false) + set(value) { + config.setBool("ui", "hide_contacts_without_phone_number_or_sip_address", value) + } + @get:WorkerThread @set:WorkerThread var contactsFilter: String get() = config.getString("ui", "contacts_filter", "")!! // Default value must be empty! diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt index 7d9eb8a493..2d223b35f7 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt @@ -276,10 +276,16 @@ class ContactsListViewModel val favouritesList = arrayListOf() var count = 0 val collator = Collator.getInstance(Locale.getDefault()) + val hideEmptyContacts = corePreferences.hideContactsWithoutPhoneNumberOrSipAddress for (result in results) { val friend = result.friend if (friend != null) { + if (hideEmptyContacts && friend.addresses.isEmpty() && friend.phoneNumbers.isEmpty()) { + Log.i("$TAG Friend [${friend.name}] has no SIP address nor phone number, do not show it") + continue + } + if (friend.refKey.orEmpty().isEmpty()) { if (friend.vcard != null) { friend.vcard?.generateUniqueId() diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 0fc439ecfb..b5166aa872 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -110,6 +110,7 @@ class SettingsViewModel AppUtils.getString(R.string.contact_editor_last_name), ) val sortContactsByValues = arrayListOf(0, 1) + val hideEmptyContacts = MutableLiveData() val ldapAvailable = MutableLiveData() val ldapServers = MutableLiveData>() @@ -339,6 +340,7 @@ class SettingsViewModel ) sortContactsBy.postValue(if (corePreferences.sortContactsByFirstName) 0 else 1) + hideEmptyContacts.postValue(corePreferences.hideContactsWithoutPhoneNumberOrSipAddress) defaultLayout.postValue(core.defaultConferenceLayout.toInt()) @@ -565,6 +567,15 @@ class SettingsViewModel } } + @UiThread + fun toggleHideEmptyContacts() { + val newValue = hideEmptyContacts.value == false + coreContext.postOnCoreThread { + corePreferences.hideContactsWithoutPhoneNumberOrSipAddress = newValue + hideEmptyContacts.postValue(newValue) + } + } + @UiThread fun addLdapServer() { addLdapServerEvent.value = Event(true) diff --git a/app/src/main/res/layout/settings_contacts.xml b/app/src/main/res/layout/settings_contacts.xml index 40d189ed4c..01dd3e5900 100644 --- a/app/src/main/res/layout/settings_contacts.xml +++ b/app/src/main/res/layout/settings_contacts.xml @@ -61,6 +61,33 @@ app:layout_constraintBottom_toBottomOf="@id/sort_contacts_by_first_name_spinner" app:layout_constraintEnd_toEndOf="@id/sort_contacts_by_first_name_spinner"/> + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b6adebfefc..6e8c8a474b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -208,6 +208,7 @@ Marquer la conversation comme lue lorsqu\'une notification de message est supprimée Contacts Trier les contacts par + Masquer les contacts sans adresse SIP ni numéro de téléphone Ajouter un serveur LDAP Editer le serveur LDAP Ajouter un carnet d\'adresse CardDAV diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 464ab90207..c07db92565 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -248,6 +248,7 @@ Mark conversation as read when dismissing message notification Contacts Sort contacts by + Hide contacts without SIP address nor phone number Add LDAP server Edit LDAP server Add CardDAV address book From 90922568b586ea1ee7845012d81caf5430970f40 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 2 Jun 2025 10:32:38 +0200 Subject: [PATCH 165/593] Prevent port from being set in third party account SIP identity + update existing accounts to remove port from identity --- .../main/java/org/linphone/core/CoreContext.kt | 18 ++++++++++++++++++ .../ThirdPartySipAccountLoginViewModel.kt | 17 ++++++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 479e199791..4b0380025b 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -639,6 +639,8 @@ class CoreContext configurationMigration5To6() } else if (oldVersion < 600004) { // 6.0.4 disablePushNotificationsFromThirdPartySipAccounts() + } else if (oldVersion < 600009) { // 6.0.9 + removePortFromSipIdentity() } if (core.logCollectionUploadServerUrl.isNullOrEmpty()) { @@ -1098,6 +1100,22 @@ class CoreContext // Migration between versions related + @WorkerThread + private fun removePortFromSipIdentity() { + for (account in core.accountList) { + val params = account.params + val identity = params.identityAddress + if (identity != null && identity.port != 0) { + val clone = params.clone() + val newIdentity = identity.clone() + newIdentity.port = 0 + clone.identityAddress = newIdentity + Log.w("$TAG Found account with identity address [${identity.asStringUriOnly()}] that contains port information in domain, removing port information in new identity [${newIdentity.asStringUriOnly()}]") + account.params = clone + } + } + } + @WorkerThread private fun disablePushNotificationsFromThirdPartySipAccounts() { for (account in core.accountList) { diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt index ac2c57db81..1b7c62adf3 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt @@ -173,11 +173,17 @@ class ThirdPartySipAccountLoginViewModel // Remove sip: in front of domain, just in case... val domainValue = domain.value.orEmpty().trim() - val domain = if (domainValue.startsWith("sip:")) { + val domainWithoutSip = if (domainValue.startsWith("sip:")) { domainValue.substring("sip:".length) } else { domainValue } + val domainAddress = Factory.instance().createAddress("sip:$domainWithoutSip") + val port = domainAddress?.port ?: -1 + if (port != -1) { + Log.w("$TAG It seems a port [$port] was set in the domain [$domainValue], removing it from SIP identity but setting it to proxy server URI") + } + val domain = domainAddress?.domain ?: domainWithoutSip // Allow to enter SIP identity instead of simply username // in case identity domain doesn't match proxy domain @@ -194,7 +200,6 @@ class ThirdPartySipAccountLoginViewModel val userId = authId.value.orEmpty().trim() Log.i("$TAG Parsed username is [$user], user ID [$userId] and domain [$domain]") - val identity = "sip:$user@$domain" val identityAddress = Factory.instance().createAddress(identity) if (identityAddress == null) { @@ -202,13 +207,14 @@ class ThirdPartySipAccountLoginViewModel showRedToast(R.string.assistant_login_cant_parse_address_toast, R.drawable.warning_circle) return@postOnCoreThread } + Log.i("$TAG Computed SIP identity is [${identityAddress.asStringUriOnly()}]") val accounts = core.accountList val found = accounts.find { it.params.identityAddress?.weakEqual(identityAddress) == true } if (found != null) { - Log.w("$TAG An account with the same identity address [${identityAddress.asStringUriOnly()}] already exists, do not add it again!") + Log.w("$TAG An account with the same identity address [${found.params.identityAddress?.asStringUriOnly()}] already exists, do not add it again!") showRedToast(R.string.assistant_account_login_already_connected_error, R.drawable.warning_circle) return@postOnCoreThread } @@ -219,7 +225,7 @@ class ThirdPartySipAccountLoginViewModel password.value.orEmpty().trim(), null, null, - domainValue + domainAddress?.domain ?: domainValue ) core.addAuthInfo(newlyCreatedAuthInfo) @@ -239,7 +245,7 @@ class ThirdPartySipAccountLoginViewModel } Factory.instance().createAddress(server) } else { - Factory.instance().createAddress("sip:$domain") + domainAddress ?: Factory.instance().createAddress("sip:$domainWithoutSip") } serverAddress?.transport = when (transport.value.orEmpty().trim()) { @@ -247,6 +253,7 @@ class ThirdPartySipAccountLoginViewModel TransportType.Tls.name.uppercase(Locale.getDefault()) -> TransportType.Tls else -> TransportType.Udp } + Log.i("$TAG Created proxy server SIP address [${serverAddress?.asStringUriOnly()}]") accountParams.serverAddress = serverAddress val prefix = internationalPrefix.value.orEmpty().trim() From 9e1d358f4e5be248cf05fdc020661c320b0c1dd7 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 2 Jun 2025 11:01:30 +0200 Subject: [PATCH 166/593] Check if outgoing early-media call is really doing video instead of assuming it is --- .../linphone/ui/call/fragment/OutgoingCallFragment.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/call/fragment/OutgoingCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/OutgoingCallFragment.kt index ffbb9714a4..a1f689e3db 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/OutgoingCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/OutgoingCallFragment.kt @@ -28,9 +28,11 @@ import androidx.core.view.doOnLayout import androidx.lifecycle.ViewModelProvider import com.google.android.material.bottomsheet.BottomSheetBehavior import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Call import org.linphone.core.tools.Log import org.linphone.databinding.CallOutgoingFragmentBinding import org.linphone.ui.call.viewmodel.CurrentCallViewModel +import org.linphone.utils.LinphoneUtils @UiThread class OutgoingCallFragment : GenericCallFragment() { @@ -66,8 +68,13 @@ class OutgoingCallFragment : GenericCallFragment() { callViewModel.isOutgoingEarlyMedia.observe(viewLifecycleOwner) { earlyMedia -> if (earlyMedia) { coreContext.postOnCoreThread { core -> - Log.i("$TAG Outgoing early-media call with video, setting preview surface") - core.nativePreviewWindowId = binding.localPreviewVideoSurface + val call = core.calls.find { + it.state == Call.State.OutgoingEarlyMedia + } + if (call != null && LinphoneUtils.isVideoEnabled(call)) { + Log.i("$TAG Outgoing early-media call with video, setting preview surface") + core.nativePreviewWindowId = binding.localPreviewVideoSurface + } } } } From dfdc26a575f38d3c8e97bb4e56c2facf1540149d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 2 Jun 2025 14:03:57 +0200 Subject: [PATCH 167/593] Prevent crash for call notification due to empty person name, using all identification fields from vCard (first & last names, organization, job title) --- .../org/linphone/contacts/ContactsManager.kt | 26 ++++++++++++++++--- .../notifications/NotificationsManager.kt | 12 ++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 9ce49e1b11..db271d0341 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -683,7 +683,7 @@ fun Friend.getAvatarBitmap(round: Boolean = false): Bitmap? { photo ?: getNativeContactPictureUri()?.toString(), round ) - } catch (numberFormatException: NumberFormatException) { + } catch (_: NumberFormatException) { // Expected for contacts created by Linphone } return null @@ -720,7 +720,7 @@ fun Friend.getNativeContactPictureUri(): Uri? { lookupUri, ContactsContract.Contacts.Photo.CONTENT_DIRECTORY ) - } catch (numberFormatException: NumberFormatException) { + } catch (_: NumberFormatException) { // Expected for contacts created by Linphone } } @@ -729,7 +729,25 @@ fun Friend.getNativeContactPictureUri(): Uri? { @WorkerThread fun Friend.getPerson(): Person { - val personBuilder = Person.Builder().setName(name) + val personBuilder = Person.Builder() + val personName = if (name.orEmpty().isNotEmpty()) { + name + } else { + if (!lastName.isNullOrEmpty() || !firstName.isNullOrEmpty()) { + Log.w("[Friend] Name is null or empty, using first and last name") + "$firstName $lastName".trim() + } else if (!organization.isNullOrEmpty()) { + Log.w("[Friend] Name, first name & last name are null or empty, using organization instead") + organization + } else if (!jobTitle.isNullOrEmpty()) { + Log.w("[Friend] Name, first and last names & organization are null or empty, using job title instead") + jobTitle + } else { + Log.e("[Friend] No identification field filled for this friend!") + "Unknown" + } + } + personBuilder.setName(personName) val bm: Bitmap? = getAvatarBitmap() personBuilder.setIcon( @@ -737,7 +755,7 @@ fun Friend.getPerson(): Person { Log.i( "[Friend] Can't use friend [$name] picture path, generating avatar based on initials" ) - AvatarGenerator(coreContext.context).setInitials(AppUtils.getInitials(name.orEmpty())).buildIcon() + AvatarGenerator(coreContext.context).setInitials(AppUtils.getInitials(personName.orEmpty())).buildIcon() } else { IconCompat.createWithAdaptiveBitmap(bm) } diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index c75dd5ecaf..1e11eab28b 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -1220,9 +1220,7 @@ class NotificationsManager } else { val contact = friend ?: coreContext.contactsManager.findContactByAddress(remoteAddress) - val displayName = contact?.name ?: LinphoneUtils.getDisplayName(remoteAddress) - - getPerson(contact, displayName) + getPerson(contact, LinphoneUtils.getDisplayName(remoteAddress)) } val isVideo = LinphoneUtils.isVideoEnabled(call) @@ -1588,14 +1586,14 @@ class NotificationsManager } @WorkerThread - private fun getPerson(friend: Friend?, displayName: String): Person { + private fun getPerson(friend: Friend?, fallbackDisplayName: String): Person { return friend?.getPerson() ?: Person.Builder() - .setName(displayName) + .setName(if (fallbackDisplayName.isEmpty()) "Unknown" else fallbackDisplayName) .setIcon( - AvatarGenerator(context).setInitials(AppUtils.getInitials(displayName)).buildIcon() + AvatarGenerator(context).setInitials(AppUtils.getInitials(fallbackDisplayName)).buildIcon() ) - .setKey(displayName) + .setKey(fallbackDisplayName) .setImportant(false) .build() } From 32060f683022cfab75d1f4536c551e745ab7cd0f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 5 Jun 2025 09:39:23 +0200 Subject: [PATCH 168/593] Bumped dependencies --- gradle/libs.versions.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4005b23b31..5f4eeaa10f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,12 +3,12 @@ agp = "8.10.1" kotlin = "2.0.21" gmsGoogleServices = "4.4.2" firebaseCrashlytics = "3.0.3" -firebaseBomVersion = "33.13.0" +firebaseBomVersion = "33.14.0" ktlint = "12.1.2" annotations = "1.9.1" activity = "1.10.1" -appcompat = "1.7.0" +appcompat = "1.7.1" constraintLayout = "2.2.1" coreKtx = "1.16.0" splashscreen = "1.2.0-beta02" @@ -16,9 +16,9 @@ telecom = "1.0.0" media = "1.7.0" recyclerview = "1.4.0" slidingpanelayout = "1.2.0" -window = "1.3.0" +window = "1.4.0" gridlayout = "1.1.0" -securityCryptoKtx = "1.1.0-alpha07" +securityCryptoKtx = "1.1.0-beta01" navigation = "2.9.0" emoji2 = "1.5.0" car = "1.7.0-rc01" From 85679dcc43beacde417a715c0e2605bf6ca6f298 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 5 Jun 2025 09:51:21 +0200 Subject: [PATCH 169/593] Added vu meter for playback volume + setting to enable vu meters (disabled by default) --- .../java/org/linphone/core/CorePreferences.kt | 4 ++++ .../org/linphone/ui/call/view/VuMeterView.kt | 4 ++-- .../ui/call/viewmodel/CurrentCallViewModel.kt | 19 ++++++++++------ .../main/res/layout/call_actions_generic.xml | 22 ++++++++++++++++++- 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 5d5a8f0022..af825302c5 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -407,6 +407,10 @@ class CorePreferences val showLettersOnDialpad: Boolean get() = config.getBool("ui", "show_letters_on_dialpad", true) + @get:WorkerThread + val showMicrophoneAndSpeakerVuMeters: Boolean + get() = config.getBool("ui", "show_mic_speaker_vu_meter", false) + // Paths @get:AnyThread diff --git a/app/src/main/java/org/linphone/ui/call/view/VuMeterView.kt b/app/src/main/java/org/linphone/ui/call/view/VuMeterView.kt index 6aa39bf1f1..4d75ab1202 100644 --- a/app/src/main/java/org/linphone/ui/call/view/VuMeterView.kt +++ b/app/src/main/java/org/linphone/ui/call/view/VuMeterView.kt @@ -68,7 +68,7 @@ class VuMeterView : View { vuMeterPaint = Paint() vuMeterPaint.strokeWidth = 2f vuMeterPaint.isAntiAlias = true - vuMeterPaint.setColor(color) + vuMeterPaint.color = color } fun setVuMeterPercentage(percentage: Float) { @@ -93,7 +93,7 @@ class VuMeterView : View { } override fun onDraw(canvas: Canvas) { - paint.setShader(createShader()) + paint.shader = createShader() canvas.drawCircle(width / 2f, height / 2f, width / 2f, paint) } } diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index c44a6f74ff..872687286b 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -116,6 +116,8 @@ class CurrentCallViewModel val microphoneRecordingVolume = MutableLiveData() + val playbackVolume = MutableLiveData() + val isSpeakerEnabled = MutableLiveData() val isHeadsetEnabled = MutableLiveData() @@ -537,6 +539,7 @@ class CurrentCallViewModel proximitySensorEnabled.value = false videoUpdateInProgress.value = false microphoneRecordingVolume.value = 0f + playbackVolume.value = 0f refreshKeyguardLockedStatus() answerAlpha.value = 1f @@ -1254,12 +1257,14 @@ class CurrentCallViewModel Log.i("$TAG Failed to find an existing 1-1 conversation for current call") } - microphoneVolumeVuMeterTickerFlow().onEach { - coreContext.postOnCoreThread { - val volumeDbm0 = currentCall.recordVolume - microphoneRecordingVolume.postValue(computeVuMeterValue(volumeDbm0)) - } - }.launchIn(viewModelScope) + if (corePreferences.showMicrophoneAndSpeakerVuMeters) { + volumeVuMeterTickerFlow().onEach { + coreContext.postOnCoreThread { + microphoneRecordingVolume.postValue(computeVuMeterValue(currentCall.recordVolume)) + playbackVolume.postValue(computeVuMeterValue(currentCall.playVolume)) + } + }.launchIn(viewModelScope) + } } @WorkerThread @@ -1530,7 +1535,7 @@ class CurrentCallViewModel showGreenToast(R.string.call_is_being_recorded, R.drawable.record_fill) } - private fun microphoneVolumeVuMeterTickerFlow() = flow { + private fun volumeVuMeterTickerFlow() = flow { while (::currentCall.isInitialized) { emit(Unit) delay(50) diff --git a/app/src/main/res/layout/call_actions_generic.xml b/app/src/main/res/layout/call_actions_generic.xml index 394b249c0b..771d99bdae 100644 --- a/app/src/main/res/layout/call_actions_generic.xml +++ b/app/src/main/res/layout/call_actions_generic.xml @@ -114,6 +114,27 @@ app:layout_constraintStart_toEndOf="@id/toggle_video" app:layout_constraintEnd_toStartOf="@id/change_audio_output" /> + + + + Date: Thu, 5 Jun 2025 10:28:58 +0200 Subject: [PATCH 170/593] Display last message timestamp in conversations list instead of chat room last updated timestamp --- .../ui/main/chat/model/ConversationModel.kt | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt index 6392b6714d..bd8cd8bfc3 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt @@ -349,12 +349,27 @@ class ConversationModel message.addListener(chatMessageListener) lastMessage = message } + + val timestamp = message.time + val humanReadableTimestamp = when { + TimestampUtils.isToday(timestamp) -> { + TimestampUtils.timeToString(timestamp) + } + TimestampUtils.isYesterday(timestamp) -> { + AppUtils.getString(R.string.yesterday) + } + else -> { + TimestampUtils.toString(timestamp, onlyDate = true) + } + } + dateTime.postValue(humanReadableTimestamp) } else { lastMessage = null lastMessageTextSender.postValue("") lastMessageContentIcon.postValue(0) lastMessageText.postValue(SpannableStringBuilder("").toSpannable()) isLastMessageOutgoing.postValue(false) + dateTime.postValue("") Log.w("$TAG No last message to display for conversation [$id]") } } @@ -362,18 +377,6 @@ class ConversationModel @WorkerThread private fun updateLastUpdatedTime() { val timestamp = chatRoom.lastUpdateTime - val humanReadableTimestamp = when { - TimestampUtils.isToday(timestamp) -> { - TimestampUtils.timeToString(chatRoom.lastUpdateTime) - } - TimestampUtils.isYesterday(timestamp) -> { - AppUtils.getString(R.string.yesterday) - } - else -> { - TimestampUtils.toString(chatRoom.lastUpdateTime, onlyDate = true) - } - } - dateTime.postValue(humanReadableTimestamp) lastUpdateTime.postValue(timestamp) } From 316bc6698a302b3b9479301e2c7a7a60afd1cb06 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 6 Jun 2025 11:44:17 +0200 Subject: [PATCH 171/593] Added user guide link in Help section, factorized code for URL opening, added missing ScrollView to help & debug layouts --- .../ui/assistant/fragment/LandingFragment.kt | 70 +-- .../chat/fragment/ConversationFragment.kt | 8 + .../ui/main/fragment/DrawerMenuFragment.kt | 4 + .../ui/main/help/fragment/HelpFragment.kt | 92 +-- app/src/main/res/drawable/book_open_text.xml | 9 + .../main/res/layout/help_debug_fragment.xml | 484 ++++++++------- app/src/main/res/layout/help_fragment.xml | 584 ++++++++++-------- app/src/main/res/values-fr/strings.xml | 4 + app/src/main/res/values/strings.xml | 7 +- 9 files changed, 660 insertions(+), 602 deletions(-) create mode 100644 app/src/main/res/drawable/book_open_text.xml diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt index e252398724..fc6bfdd47a 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt @@ -104,22 +104,7 @@ class LandingFragment : GenericFragment() { binding.setForgottenPasswordClickListener { val url = getString(R.string.web_platform_forgotten_password_url) - try { - val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) - startActivity(browserIntent) - } catch (ise: IllegalStateException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" - ) - } catch (anfe: ActivityNotFoundException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" - ) - } catch (e: Exception) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" - ) - } + openUrlInBrowser(url) } viewModel.showPassword.observe(viewLifecycleOwner) { @@ -215,47 +200,36 @@ class LandingFragment : GenericFragment() { model.privacyPolicyClickedEvent.observe(viewLifecycleOwner) { it.consume { val url = getString(R.string.website_privacy_policy_url) - try { - val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) - startActivity(browserIntent) - } catch (ise: IllegalStateException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" - ) - } catch (anfe: ActivityNotFoundException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" - ) - } catch (e: Exception) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" - ) - } + openUrlInBrowser(url) } } model.generalTermsClickedEvent.observe(viewLifecycleOwner) { it.consume { val url = getString(R.string.website_terms_and_conditions_url) - try { - val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) - startActivity(browserIntent) - } catch (ise: IllegalStateException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" - ) - } catch (anfe: ActivityNotFoundException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" - ) - } catch (e: Exception) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" - ) - } + openUrlInBrowser(url) } } dialog.show() } + + private fun openUrlInBrowser(url: String) { + try { + val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) + startActivity(browserIntent) + } catch (ise: IllegalStateException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" + ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" + ) + } catch (e: Exception) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" + ) + } + } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 1fe062ec86..85b6caea0b 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -796,6 +796,14 @@ open class ConversationFragment : SlidingPaneChildFragment() { try { val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) startActivity(browserIntent) + } catch (ise: IllegalStateException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" + ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" + ) } catch (e: Exception) { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" diff --git a/app/src/main/java/org/linphone/ui/main/fragment/DrawerMenuFragment.kt b/app/src/main/java/org/linphone/ui/main/fragment/DrawerMenuFragment.kt index a85fa47853..ce762f7de3 100644 --- a/app/src/main/java/org/linphone/ui/main/fragment/DrawerMenuFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/fragment/DrawerMenuFragment.kt @@ -157,6 +157,10 @@ class DrawerMenuFragment : GenericMainFragment() { Log.e( "$TAG Can't start ACTION_VIEW intent for URL [$link], ActivityNotFoundException: $anfe" ) + } catch (e: Exception) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$link]: $e" + ) } } } diff --git a/app/src/main/java/org/linphone/ui/main/help/fragment/HelpFragment.kt b/app/src/main/java/org/linphone/ui/main/help/fragment/HelpFragment.kt index 4413b92b0e..4281c1160b 100644 --- a/app/src/main/java/org/linphone/ui/main/help/fragment/HelpFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/help/fragment/HelpFragment.kt @@ -76,64 +76,24 @@ class HelpFragment : GenericMainFragment() { } } + binding.setUserGuideClickListener { + val url = getString(R.string.website_user_guide_url) + openUrlInBrowser(url) + } + binding.setPrivacyPolicyClickListener { val url = getString(R.string.website_privacy_policy_url) - try { - val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) - startActivity(browserIntent) - } catch (ise: IllegalStateException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" - ) - } catch (anfe: ActivityNotFoundException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" - ) - } catch (e: Exception) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" - ) - } + openUrlInBrowser(url) } binding.setLicensesClickListener { val url = getString(R.string.website_open_source_licences_usage_url) - try { - val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) - startActivity(browserIntent) - } catch (ise: IllegalStateException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" - ) - } catch (anfe: ActivityNotFoundException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" - ) - } catch (e: Exception) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" - ) - } + openUrlInBrowser(url) } binding.setTranslateClickListener { val url = getString(R.string.website_translate_weblate_url) - try { - val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) - startActivity(browserIntent) - } catch (ise: IllegalStateException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" - ) - } catch (anfe: ActivityNotFoundException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" - ) - } catch (e: Exception) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" - ) - } + openUrlInBrowser(url) } viewModel.newVersionAvailableEvent.observe(viewLifecycleOwner) { @@ -181,26 +141,30 @@ class HelpFragment : GenericMainFragment() { model.confirmEvent.observe(viewLifecycleOwner) { it.consume { - try { - val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) - startActivity(browserIntent) - } catch (ise: IllegalStateException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" - ) - } catch (anfe: ActivityNotFoundException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" - ) - } catch (e: Exception) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" - ) - } + openUrlInBrowser(url) dialog.dismiss() } } dialog.show() } + + private fun openUrlInBrowser(url: String) { + try { + val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) + startActivity(browserIntent) + } catch (ise: IllegalStateException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" + ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" + ) + } catch (e: Exception) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" + ) + } + } } diff --git a/app/src/main/res/drawable/book_open_text.xml b/app/src/main/res/drawable/book_open_text.xml new file mode 100644 index 0000000000..2c0c55c301 --- /dev/null +++ b/app/src/main/res/drawable/book_open_text.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/help_debug_fragment.xml b/app/src/main/res/layout/help_debug_fragment.xml index 56e9745b1f..9d38cae41c 100644 --- a/app/src/main/res/layout/help_debug_fragment.xml +++ b/app/src/main/res/layout/help_debug_fragment.xml @@ -54,263 +54,271 @@ app:layout_constraintStart_toEndOf="@id/back" app:layout_constraintTop_toTopOf="parent"/> - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/back" + app:layout_constraintBottom_toBottomOf="parent"> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + + + + + @@ -318,6 +326,6 @@ layout="@layout/operation_in_progress" bind:visibility="@{viewModel.logsUploadInProgress}" /> - + \ No newline at end of file diff --git a/app/src/main/res/layout/help_fragment.xml b/app/src/main/res/layout/help_fragment.xml index f8fd0e3121..5e3f4711f6 100644 --- a/app/src/main/res/layout/help_fragment.xml +++ b/app/src/main/res/layout/help_fragment.xml @@ -8,6 +8,9 @@ + @@ -25,15 +28,14 @@ type="org.linphone.ui.main.help.viewmodel.HelpViewModel" /> - + android:layout_height="match_parent"> + android:layout_height="match_parent" + android:background="?attr/color_background_contrast_in_dark_mode"> - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/back" + app:layout_constraintBottom_toBottomOf="parent"> - + - + - + - + - + - - - - - - - + - - - + - + - + - + - + - + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6e8c8a474b..bb5a546eff 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -153,11 +153,14 @@ Aide À propos de &appName; + Guide utilisateur &appName; + Apprenez à maîtriser toutes les fonctionnalités de l\'application, pas à pas. Politique de confidentialité Quelles informations &appName; collecte et utilise Version Vérifier les mises à jour Aider à traduire &appName; + Contribuez à rendre l\'application accessible au plus grand nombre. Avancé Votre version est à jour Une erreur est survenue @@ -165,6 +168,7 @@ Une nouvelle version %s est disponible. Voulez-vous mettre à jour ? Quitter l\'application Dépannage + Transmettez vos journaux de diagnostic pour faciliter la résolution des bugs. Imprimer les journaux dans logcat Nettoyer les journaux Partager les journaux diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c07db92565..42be308f7b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,8 +32,9 @@ https://linphone.org/contact https://linphone.org/linphone-softphone + https://linphone.org/en/docs/ https://linphone.org/en/privacy-policy - https://www.linphone.org/en/terms-of-use + https://linphone.org/en/terms-of-use https://subscribe.linphone.org/register/email https://subscribe.linphone.org/ https://weblate.linphone.org/ @@ -193,11 +194,14 @@ Help About &appName; + &appName; user guide + Learn how to master all app features, step by step. Privacy policy What information &appName; collects and uses Version Check update Contribute on &appName; translation + Help make the app accessible to as many people as possible. Advanced Your version is up-to-date An error occurred while checking for update @@ -205,6 +209,7 @@ A new version %s is available. Do you want to update? Quit app Troubleshooting + Transmit your diagnostic logs to facilitate bug resolution. Print logs in logcat Clean logs Share logs From cb27b35984c945de74433b26a8fa8a8a1978ca84 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 6 Jun 2025 15:47:32 +0200 Subject: [PATCH 172/593] Updated changelog & version code/name from release/6.0 branch --- CHANGELOG.md | 18 +++++++++++++++++- app/build.gradle.kts | 6 +++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48d6c6a4cb..34bb72c3a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,13 +13,29 @@ Group changes to describe their impact on the project, as follows: ## [6.1.0] - Unreleased ### Added -- Added a vu meter for recording volume +- Added a vu meter for recording & playback volumes (disabled by default, must be enabled in CorePreferences) - Added a setting for user to choose whether to sort contacts by first name or last name +- Added a setting to hide contacts that have neither a SIP address nor a phone number - Added a setting to let app auto-answer call with video sending already enabled ### Changed - Reworked some settings (moved calls related ones from advanced settings to advanced calls settings) +## [6.0.9] - 2025-06-06 + +### Added +- German translation (88% complete) +- Link to user guide in Help section +- Missing scroll views for help & debug layouts + +### Changed +- Prevent port from being set in the SIP identity address in third party account login + remove port (if any) from SIP identity for existing accounts +- Show last message timestamp instead of conversation last updated timestamp in conversations list + +### Fixed +- Prevent blinking in conversations list when removing message from chat room +- Prevent empty (can even lead to crash) display name in call notification (using all identification fields from vCard) + ## [6.0.8] - 2025-05-23 ### Added diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d9ad569f7e..bbb985f72e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,7 +32,7 @@ if (firebaseCloudMessagingAvailable) { } var gitBranch = ByteArrayOutputStream() -var gitVersion = "6.0.8" +var gitVersion = "6.0.9" task("getGitVersion") { val gitVersionStream = ByteArrayOutputStream() @@ -100,8 +100,8 @@ android { applicationId = packageName minSdk = 28 targetSdk = 36 - versionCode = 600008 // 6.00.008 - versionName = "6.0.8" + versionCode = 600009 // 6.00.009 + versionName = "6.0.9" manifestPlaceholders["appAuthRedirectScheme"] = packageName From 2bd0de4af19e5be908899b736cd70cee267bab43 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 10 Jun 2025 12:07:45 +0200 Subject: [PATCH 173/593] Fixed group conversation creation if LIME server URL not set --- .../ui/main/chat/viewmodel/StartConversationViewModel.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/StartConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/StartConversationViewModel.kt index 3e7c261563..4523e96d9b 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/StartConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/StartConversationViewModel.kt @@ -111,7 +111,11 @@ class StartConversationViewModel params.isChatEnabled = true params.isGroupEnabled = true params.subject = groupChatRoomSubject - params.securityLevel = Conference.SecurityLevel.EndToEnd + if (LinphoneUtils.isEndToEndEncryptedChatAvailable(core)) { + params.securityLevel = Conference.SecurityLevel.EndToEnd + } else { + params.securityLevel = Conference.SecurityLevel.None + } params.account = account val chatParams = params.chatParams ?: return@postOnCoreThread From ae39d79420b23852aea22c3cf392e2539ed48a87 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 13 Jun 2025 09:20:01 +0200 Subject: [PATCH 174/593] Various improvements --- README.md | 2 +- .../ui/main/chat/model/ConversationModel.kt | 6 ++-- .../ui/main/chat/model/EventLogModel.kt | 28 ----------------- .../ui/main/chat/model/MessageModel.kt | 31 ++++++++++++++++--- .../layout/chat_bubble_long_press_menu.xml | 6 ++-- 5 files changed, 35 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index b81fc5fadd..4e2156060c 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Linphone is dual licensed, and is available either : 6.0.0 release is a completely new version, designed with UX/UI experts and marks a turning point in design, features, and user experience. The improvements make this version smoother and simpler for both developers and users. -You can take a look at the [CHANGELOG.md](CHANGELOG.md) file for a non-exhaustive list of changes of this new version and of the newly added features, the most exciting ones being the improved fluidity, a real multi-accounts support and asymetrical video in calls. +You can take a look at the [CHANGELOG.md](CHANGELOG.md) file for a non-exhaustive list of changes of this new version and of the newly added features, the most exciting ones being the improved fluidity, a real multi-accounts support and asymmetrical video in calls. This release only works on Android OS 9.0 and newer. diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt index bd8cd8bfc3..63288eb0ff 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt @@ -107,6 +107,7 @@ class ConversationModel } } + @WorkerThread override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) { // This is required as a Created chat room may not have the participants list yet Log.i("$TAG Conversation has been joined") @@ -129,10 +130,12 @@ class ConversationModel computeComposingLabel() } + @WorkerThread override fun onNewEvent(chatRoom: ChatRoom, eventLog: EventLog) { updateLastUpdatedTime() } + @WorkerThread override fun onNewEvents(chatRoom: ChatRoom, eventLogs: Array) { updateLastMessage() updateLastUpdatedTime() @@ -340,14 +343,13 @@ class ConversationModel val message = chatRoom.lastMessageInHistory if (message != null) { + lastMessage = message updateLastMessageStatus(message) if (message.isOutgoing && message.state != ChatMessage.State.Displayed) { message.addListener(chatMessageListener) - lastMessage = message } else if (message.contents.find { it.isFileTransfer == true } != null) { message.addListener(chatMessageListener) - lastMessage = message } val timestamp = message.time diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/EventLogModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/EventLogModel.kt index 08b08dcb91..b0da4ae9d1 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/EventLogModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/EventLogModel.kt @@ -20,10 +20,7 @@ package org.linphone.ui.main.chat.model import androidx.annotation.WorkerThread -import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.core.EventLog -import org.linphone.core.tools.Log -import org.linphone.utils.LinphoneUtils class EventLogModel @WorkerThread @@ -54,35 +51,10 @@ class EventLogModel EventModel(eventLog) } else { val chatMessage = eventLog.chatMessage!! - var replyTo = "" - var isReply = chatMessage.isReply - val replyText = if (chatMessage.isReply) { - val replyMessage = chatMessage.replyMessage - if (replyMessage != null) { - val from = replyMessage.fromAddress - val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(from) - replyTo = avatarModel.contactName ?: LinphoneUtils.getDisplayName(from) - - LinphoneUtils.getPlainTextDescribingMessage(replyMessage) - } else { - Log.e( - "$TAG Failed to find the reply message from ID [${chatMessage.replyMessageId}]" - ) - isReply = false - "" - } - } else { - "" - } MessageModel( chatMessage, isFromGroup, - isReply, - replyTo, - replyText, - chatMessage.replyMessageId, - chatMessage.isForward, isGroupedWithPreviousOne, isGroupedWithNextOne, currentFilter, diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt index fe6968ac50..8e1880e34f 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt @@ -70,11 +70,6 @@ class MessageModel constructor( val chatMessage: ChatMessage, val isFromGroup: Boolean, - val isReply: Boolean, - val replyTo: String, - val replyText: String, - val replyToMessageId: String?, - val isForward: Boolean, isGroupedWithPreviousOne: Boolean, isGroupedWithNextOne: Boolean, private val currentFilter: String = "", @@ -116,6 +111,16 @@ class MessageModel )?.params?.instantMessagingEncryptionMandatory == true ) + val isReply = chatMessage.isReply + + val replyToMessageId = chatMessage.replyMessageId + + val isForward = chatMessage.isForward + + val replyTo = MutableLiveData() + + val replyText = MutableLiveData() + val avatarModel = MutableLiveData() val groupedWithNextMessage = MutableLiveData() @@ -314,6 +319,9 @@ class MessageModel updateReactionsList() computeContentsList() + if (isReply) { + computeReplyInfo() + } coreContext.postOnMainThread { firstFileModel.addSource(filesList) { @@ -629,6 +637,19 @@ class MessageModel } } + @WorkerThread + fun computeReplyInfo() { + val replyMessage = chatMessage.replyMessage + if (replyMessage != null) { + val from = replyMessage.fromAddress + val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(from) + replyTo.postValue(avatarModel.contactName ?: LinphoneUtils.getDisplayName(from)) + replyText.postValue(LinphoneUtils.getFormattedTextDescribingMessage(replyMessage)) + } else { + Log.e("$TAG Failed to find the reply message from ID [${chatMessage.replyMessageId}]") + } + } + @WorkerThread private fun computeTextContent(content: Content, highlight: String) { val textContent = content.utf8Text.orEmpty().trim() diff --git a/app/src/main/res/layout/chat_bubble_long_press_menu.xml b/app/src/main/res/layout/chat_bubble_long_press_menu.xml index c6eeb2d50e..4df2e93b71 100644 --- a/app/src/main/res/layout/chat_bubble_long_press_menu.xml +++ b/app/src/main/res/layout/chat_bubble_long_press_menu.xml @@ -139,7 +139,8 @@ + android:background="?attr/color_separator" + android:visibility="@{viewModel.isChatRoomReadOnly ? View.GONE : View.VISIBLE}" /> + android:background="?attr/color_separator" + android:visibility="@{viewModel.hideForward ? View.GONE : View.VISIBLE}" /> Date: Fri, 13 Jun 2025 11:25:28 +0200 Subject: [PATCH 175/593] Added enable/disable speaker to active call notification --- CHANGELOG.md | 1 + .../NotificationBroadcastReceiver.kt | 66 ++++++++++------ .../notifications/NotificationsManager.kt | 75 +++++++++++++++---- app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 110 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34bb72c3a8..6f8618f166 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Group changes to describe their impact on the project, as follows: ## [6.1.0] - Unreleased ### Added +- Added toggle speaker action in active call notification - Added a vu meter for recording & playback volumes (disabled by default, must be enabled in CorePreferences) - Added a setting for user to choose whether to sort contacts by first name or last name - Added a setting to hide contacts that have neither a SIP address nor a phone number diff --git a/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt b/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt index ef9ff05660..7b98ba8385 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt @@ -26,8 +26,10 @@ import android.content.Context import android.content.Intent import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.core.Address +import org.linphone.core.AudioDevice import org.linphone.core.ConferenceParams import org.linphone.core.tools.Log +import org.linphone.utils.AudioUtils class NotificationBroadcastReceiver : BroadcastReceiver() { companion object { @@ -36,47 +38,69 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val notificationId = intent.getIntExtra(NotificationsManager.INTENT_NOTIF_ID, 0) - Log.i( - "$TAG Got notification broadcast for ID [$notificationId]" - ) + val action = intent.action + Log.i("$TAG Got notification broadcast for ID [$notificationId] with action [$action]") // Wait for coreContext to be ready to handle intent while (!coreContext.isReady()) { Thread.sleep(50) } - if (intent.action == NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION || intent.action == NotificationsManager.INTENT_HANGUP_CALL_NOTIF_ACTION) { - handleCallIntent(intent, notificationId) - } else if (intent.action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION || intent.action == NotificationsManager.INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION) { - handleChatIntent(context, intent, notificationId) + if ( + action == NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION || + action == NotificationsManager.INTENT_HANGUP_CALL_NOTIF_ACTION || + action == NotificationsManager.INTENT_TOGGLE_SPEAKER_CALL_NOTIF_ACTION + ) { + handleCallIntent(intent, notificationId, action) + } else if ( + action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION || + action == NotificationsManager.INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION + ) { + handleChatIntent(context, intent, notificationId, action) } } - private fun handleCallIntent(intent: Intent, notificationId: Int) { - val remoteSipAddress = intent.getStringExtra(NotificationsManager.INTENT_REMOTE_ADDRESS) - if (remoteSipAddress == null) { + private fun handleCallIntent(intent: Intent, notificationId: Int, action: String) { + val remoteSipUri = intent.getStringExtra(NotificationsManager.INTENT_REMOTE_SIP_URI) + if (remoteSipUri == null) { Log.e("$TAG Remote SIP address is null for call notification ID [$notificationId]") return } coreContext.postOnCoreThread { core -> val call = core.calls.find { - it.remoteAddress.asStringUriOnly() == remoteSipAddress + it.remoteAddress.asStringUriOnly() == remoteSipUri } if (call == null) { - Log.e("$TAG Couldn't find call from remote address [$remoteSipAddress]") + Log.e("$TAG Couldn't find call from remote address [$remoteSipUri]") } else { - if (intent.action == NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION) { - coreContext.answerCall(call) - } else { - coreContext.terminateCall(call) + when (action) { + NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION -> { + Log.i("$TAG Answering call with remote address [$remoteSipUri]") + coreContext.answerCall(call) + } + NotificationsManager.INTENT_HANGUP_CALL_NOTIF_ACTION -> { + Log.i("$TAG Declining/terminating call with remote address [$remoteSipUri]") + coreContext.terminateCall(call) + } + NotificationsManager.INTENT_TOGGLE_SPEAKER_CALL_NOTIF_ACTION -> { + val audioDevice = call.outputAudioDevice + val isUsingSpeaker = audioDevice?.type == AudioDevice.Type.Speaker + if (isUsingSpeaker) { + Log.i("$TAG Routing audio to earpiece for call [$remoteSipUri]") + AudioUtils.routeAudioToEarpiece(call) + } else { + Log.i("$TAG Routing audio to speaker for call [$remoteSipUri]") + AudioUtils.routeAudioToSpeaker(call) + } + } } } } } - private fun handleChatIntent(context: Context, intent: Intent, notificationId: Int) { - val remoteSipAddress = intent.getStringExtra(NotificationsManager.INTENT_REMOTE_ADDRESS) + private fun handleChatIntent(context: Context, intent: Intent, notificationId: Int, action: String) { + val remoteSipAddress = intent.getStringExtra(NotificationsManager.INTENT_REMOTE_SIP_URI) if (remoteSipAddress == null) { Log.e("$TAG Remote SIP address is null for notification ID [$notificationId]") return @@ -88,7 +112,7 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { } val reply = getMessageText(intent)?.toString() - if (intent.action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION) { + if (action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION) { if (reply == null) { Log.e("$TAG Couldn't get reply text") return @@ -128,13 +152,13 @@ class NotificationBroadcastReceiver : BroadcastReceiver() { return@postOnCoreThread } - if (intent.action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION) { + if (action == NotificationsManager.INTENT_REPLY_MESSAGE_NOTIF_ACTION) { val msg = room.createMessageFromUtf8(reply) msg.userData = notificationId msg.addListener(coreContext.notificationsManager.chatMessageListener) msg.send() Log.i("$TAG Reply sent for notif id [$notificationId]") - } else if (intent.action == NotificationsManager.INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION) { + } else if (action == NotificationsManager.INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION) { Log.i("$TAG Marking chat room from notification id [$notificationId] as read") room.markAsRead() if (!coreContext.notificationsManager.dismissChatNotification(room)) { diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 1e11eab28b..1f5bea5f2f 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -54,6 +54,7 @@ import org.linphone.contacts.ContactsManager.ContactsListener import org.linphone.contacts.getAvatarBitmap import org.linphone.contacts.getPerson import org.linphone.core.Address +import org.linphone.core.AudioDevice import org.linphone.core.Call import org.linphone.core.ChatMessage import org.linphone.core.ChatMessageListener @@ -86,13 +87,14 @@ class NotificationsManager const val INTENT_HANGUP_CALL_NOTIF_ACTION = "org.linphone.HANGUP_CALL_ACTION" const val INTENT_ANSWER_CALL_NOTIF_ACTION = "org.linphone.ANSWER_CALL_ACTION" + const val INTENT_TOGGLE_SPEAKER_CALL_NOTIF_ACTION = "org.linphone.TOGGLE_SPEAKER_CALL_ACTION" const val INTENT_REPLY_MESSAGE_NOTIF_ACTION = "org.linphone.REPLY_ACTION" const val INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION = "org.linphone.MARK_AS_READ_ACTION" const val INTENT_NOTIF_ID = "NOTIFICATION_ID" const val KEY_TEXT_REPLY = "key_text_reply" const val INTENT_LOCAL_IDENTITY = "LOCAL_IDENTITY" - const val INTENT_REMOTE_ADDRESS = "REMOTE_ADDRESS" + const val INTENT_REMOTE_SIP_URI = "REMOTE_ADDRESS" const val CHAT_TAG = "Chat" private const val MISSED_CALL_TAG = "Missed call" @@ -151,7 +153,12 @@ class NotificationsManager Log.i( "$TAG Found call [${addressMatch.asStringUriOnly()}] with contact in notifications, updating it" ) - updateCallNotification(notifiable, addressMatch, friend) + val call = coreContext.core.getCallByRemoteAddress2(addressMatch) + if (call == null) { + Log.e("$TAG Failed to get Call from Core using remote address [${addressMatch.asStringUriOnly()}]") + return + } + updateCallNotification(notifiable, call, friend) } } @@ -276,6 +283,18 @@ class NotificationsManager } } + @WorkerThread + override fun onAudioDeviceChanged(core: Core, audioDevice: AudioDevice) { + if (core.callsNb == 0) return + + val call = core.currentCall ?: core.calls.firstOrNull() + if (call != null) { + Log.i("$TAG Audio device changed, updating call [${call.remoteAddress.asStringUriOnly()}] notification") + val notifiable = getNotifiableForCall(call) + updateCallNotification(notifiable, call, null) + } + } + @WorkerThread override fun onMessagesReceived( core: Core, @@ -1292,22 +1311,33 @@ class NotificationsManager setFullScreenIntent(pendingIntent, true) } + if (!isIncoming) { + val toggleSpeakerIntent = getCallToggleSpeakerPendingIntent(notifiable) + + val audioDevice = call.outputAudioDevice + val isUsingSpeaker = audioDevice?.type == AudioDevice.Type.Speaker + + val toggleSpeakerAction = if (isUsingSpeaker) { + Log.i("$TAG Call is using speaker, adding action to disable it") + val text = AppUtils.getString(R.string.notification_disable_speaker_for_call) + NotificationCompat.Action.Builder(R.drawable.speaker_slash, text, toggleSpeakerIntent).build() + } else { + Log.i("$TAG Call is not using speaker, adding action to enable it") + val text = AppUtils.getString(R.string.notification_enable_speaker_for_call) + NotificationCompat.Action.Builder(R.drawable.speaker_high, text, toggleSpeakerIntent).build() + } + builder.addAction(toggleSpeakerAction) + } + return builder.build() } @WorkerThread private fun updateCallNotification( notifiable: Notifiable, - remoteAddress: Address, - friend: Friend + call: Call, + friend: Friend? ) { - val call = coreContext.core.getCallByRemoteAddress2(remoteAddress) - if (call == null) { - Log.w( - "$TAG Failed to find call with remote SIP URI [${remoteAddress.asStringUriOnly()}]" - ) - return - } val isIncoming = LinphoneUtils.isCallIncoming(call.state) val notification = if (isIncoming) { @@ -1469,7 +1499,7 @@ class NotificationsManager val hangupIntent = Intent(context, NotificationBroadcastReceiver::class.java) hangupIntent.action = INTENT_HANGUP_CALL_NOTIF_ACTION hangupIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) - hangupIntent.putExtra(INTENT_REMOTE_ADDRESS, notifiable.remoteAddress) + hangupIntent.putExtra(INTENT_REMOTE_SIP_URI, notifiable.remoteAddress) return PendingIntent.getBroadcast( context, @@ -1484,7 +1514,7 @@ class NotificationsManager val answerIntent = Intent(context, NotificationBroadcastReceiver::class.java) answerIntent.action = INTENT_ANSWER_CALL_NOTIF_ACTION answerIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) - answerIntent.putExtra(INTENT_REMOTE_ADDRESS, notifiable.remoteAddress) + answerIntent.putExtra(INTENT_REMOTE_SIP_URI, notifiable.remoteAddress) return PendingIntent.getBroadcast( context, @@ -1494,6 +1524,21 @@ class NotificationsManager ) } + @AnyThread + fun getCallToggleSpeakerPendingIntent(notifiable: Notifiable): PendingIntent { + val answerIntent = Intent(context, NotificationBroadcastReceiver::class.java) + answerIntent.action = INTENT_TOGGLE_SPEAKER_CALL_NOTIF_ACTION + answerIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) + answerIntent.putExtra(INTENT_REMOTE_SIP_URI, notifiable.remoteAddress) + + return PendingIntent.getBroadcast( + context, + 4, + answerIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } + @WorkerThread private fun displayReplyMessageNotification(message: ChatMessage, notifiable: Notifiable) { Log.i( @@ -1535,7 +1580,7 @@ class NotificationsManager replyIntent.action = INTENT_REPLY_MESSAGE_NOTIF_ACTION replyIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) replyIntent.putExtra(INTENT_LOCAL_IDENTITY, notifiable.localIdentity) - replyIntent.putExtra(INTENT_REMOTE_ADDRESS, notifiable.remoteAddress) + replyIntent.putExtra(INTENT_REMOTE_SIP_URI, notifiable.remoteAddress) // PendingIntents attached to actions with remote inputs must be mutable val replyPendingIntent = PendingIntent.getBroadcast( @@ -1562,7 +1607,7 @@ class NotificationsManager markAsReadIntent.action = INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION markAsReadIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) markAsReadIntent.putExtra(INTENT_LOCAL_IDENTITY, notifiable.localIdentity) - markAsReadIntent.putExtra(INTENT_REMOTE_ADDRESS, notifiable.remoteAddress) + markAsReadIntent.putExtra(INTENT_REMOTE_SIP_URI, notifiable.remoteAddress) return PendingIntent.getBroadcast( context, diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index bb5a546eff..9fef4ace3a 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -69,6 +69,8 @@ %s fichiers en cours de réception Cliquez pour ouvrir + Activer haut-parleur + Désactiver haut-parleur Bienvenue diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 42be308f7b..cccb21e021 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -110,6 +110,8 @@ %s, %s Click to open + Turn on speaker + Turn off speaker Welcome From 61be1d21d525eda4d6d59e6e13643da365dde517 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 13 Jun 2025 12:03:16 +0200 Subject: [PATCH 176/593] Improved UI on large tablets --- CHANGELOG.md | 1 + .../main/res/drawable/address_book_fill.xml | 9 + .../res/drawable/chat_teardrop_text_fill.xml | 9 + app/src/main/res/drawable/phone_fill.xml | 9 + .../shape_desktop_nav_back_background.xml | 5 + .../res/drawable/video_conference_fill.xml | 9 + .../main/res/layout-land/bottom_nav_bar.xml | 4 + .../res/layout-land/chat_list_fragment.xml | 6 + .../layout-land/contacts_list_fragment.xml | 8 +- .../res/layout-land/history_list_fragment.xml | 6 + .../layout-land/meetings_list_fragment.xml | 6 + .../layout-sw600dp-land/bottom_nav_bar.xml | 142 ++++++++++++++++ .../main_activity_top_bar.xml | 159 ++++++++++++++++++ .../sliding_pane_separator.xml | 7 + .../res/layout/contacts_list_fragment.xml | 2 +- app/src/main/res/layout/help_fragment.xml | 5 +- .../res/layout/sliding_pane_separator.xml | 6 + app/src/main/res/values-sw600dp/dimen.xml | 2 + app/src/main/res/values/dimen.xml | 3 +- app/src/main/res/values/styles.xml | 2 +- 20 files changed, 395 insertions(+), 5 deletions(-) create mode 100644 app/src/main/res/drawable/address_book_fill.xml create mode 100644 app/src/main/res/drawable/chat_teardrop_text_fill.xml create mode 100644 app/src/main/res/drawable/phone_fill.xml create mode 100644 app/src/main/res/drawable/shape_desktop_nav_back_background.xml create mode 100644 app/src/main/res/drawable/video_conference_fill.xml create mode 100644 app/src/main/res/layout-sw600dp-land/bottom_nav_bar.xml create mode 100644 app/src/main/res/layout-sw600dp-land/main_activity_top_bar.xml create mode 100644 app/src/main/res/layout-sw600dp-land/sliding_pane_separator.xml create mode 100644 app/src/main/res/layout/sliding_pane_separator.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f8618f166..b06d2ba232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Group changes to describe their impact on the project, as follows: - Added a setting to let app auto-answer call with video sending already enabled ### Changed +- Improved UI on tablets with screen sw600dp and higher, will look more like our desktop app - Reworked some settings (moved calls related ones from advanced settings to advanced calls settings) ## [6.0.9] - 2025-06-06 diff --git a/app/src/main/res/drawable/address_book_fill.xml b/app/src/main/res/drawable/address_book_fill.xml new file mode 100644 index 0000000000..80bd09ea37 --- /dev/null +++ b/app/src/main/res/drawable/address_book_fill.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/chat_teardrop_text_fill.xml b/app/src/main/res/drawable/chat_teardrop_text_fill.xml new file mode 100644 index 0000000000..901ca36e31 --- /dev/null +++ b/app/src/main/res/drawable/chat_teardrop_text_fill.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/phone_fill.xml b/app/src/main/res/drawable/phone_fill.xml new file mode 100644 index 0000000000..b47bd61b76 --- /dev/null +++ b/app/src/main/res/drawable/phone_fill.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/shape_desktop_nav_back_background.xml b/app/src/main/res/drawable/shape_desktop_nav_back_background.xml new file mode 100644 index 0000000000..dc26636f75 --- /dev/null +++ b/app/src/main/res/drawable/shape_desktop_nav_back_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/video_conference_fill.xml b/app/src/main/res/drawable/video_conference_fill.xml new file mode 100644 index 0000000000..b738875f10 --- /dev/null +++ b/app/src/main/res/drawable/video_conference_fill.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout-land/bottom_nav_bar.xml b/app/src/main/res/layout-land/bottom_nav_bar.xml index 8ddb35647c..c0bffdb5fd 100644 --- a/app/src/main/res/layout-land/bottom_nav_bar.xml +++ b/app/src/main/res/layout-land/bottom_nav_bar.xml @@ -54,6 +54,8 @@ android:id="@+id/missed_calls" android:layout_width="@dimen/unread_count_indicator_size" android:layout_height="@dimen/unread_count_indicator_size" + android:layout_marginEnd="1dp" + android:layout_marginTop="-12dp" android:text="@{String.valueOf(viewModel.missedCallsCount), default=`1`}" android:visibility="@{viewModel.missedCallsCount > 0 ? View.VISIBLE : View.GONE}" app:layout_constraintTop_toTopOf="@id/calls" @@ -81,6 +83,8 @@ android:id="@+id/unread_messages" android:layout_width="@dimen/unread_count_indicator_size" android:layout_height="@dimen/unread_count_indicator_size" + android:layout_marginEnd="1dp" + android:layout_marginTop="-12dp" android:text="@{String.valueOf(viewModel.unreadMessages), default=`100`}" android:visibility="@{viewModel.unreadMessages > 0 && !viewModel.hideConversations ? View.VISIBLE : View.GONE}" app:layout_constraintTop_toTopOf="@id/conversations" diff --git a/app/src/main/res/layout-land/chat_list_fragment.xml b/app/src/main/res/layout-land/chat_list_fragment.xml index b804759a37..566aaf44e5 100644 --- a/app/src/main/res/layout-land/chat_list_fragment.xml +++ b/app/src/main/res/layout-land/chat_list_fragment.xml @@ -148,6 +148,12 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" /> + + diff --git a/app/src/main/res/layout-land/contacts_list_fragment.xml b/app/src/main/res/layout-land/contacts_list_fragment.xml index 47db3ea5c4..b908fad2f4 100644 --- a/app/src/main/res/layout-land/contacts_list_fragment.xml +++ b/app/src/main/res/layout-land/contacts_list_fragment.xml @@ -80,7 +80,7 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:layout_marginTop="20dp" + android:layout_marginTop="10dp" android:text="@string/contacts_list_favourites_title" android:drawableEnd="@{viewModel.showFavourites ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="@color/gray_main2_600"/> @@ -186,6 +186,12 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" /> + + diff --git a/app/src/main/res/layout-land/history_list_fragment.xml b/app/src/main/res/layout-land/history_list_fragment.xml index 8ea1a46ef3..f4631b56e5 100644 --- a/app/src/main/res/layout-land/history_list_fragment.xml +++ b/app/src/main/res/layout-land/history_list_fragment.xml @@ -158,6 +158,12 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" /> + + diff --git a/app/src/main/res/layout-land/meetings_list_fragment.xml b/app/src/main/res/layout-land/meetings_list_fragment.xml index cd9832939c..d833a18e84 100644 --- a/app/src/main/res/layout-land/meetings_list_fragment.xml +++ b/app/src/main/res/layout-land/meetings_list_fragment.xml @@ -101,6 +101,12 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp-land/main_activity_top_bar.xml b/app/src/main/res/layout-sw600dp-land/main_activity_top_bar.xml new file mode 100644 index 0000000000..8b72d03133 --- /dev/null +++ b/app/src/main/res/layout-sw600dp-land/main_activity_top_bar.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw600dp-land/sliding_pane_separator.xml b/app/src/main/res/layout-sw600dp-land/sliding_pane_separator.xml new file mode 100644 index 0000000000..1c6b7c2060 --- /dev/null +++ b/app/src/main/res/layout-sw600dp-land/sliding_pane_separator.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/contacts_list_fragment.xml b/app/src/main/res/layout/contacts_list_fragment.xml index 4dc30a0b68..ab8f745394 100644 --- a/app/src/main/res/layout/contacts_list_fragment.xml +++ b/app/src/main/res/layout/contacts_list_fragment.xml @@ -80,7 +80,7 @@ android:padding="5dp" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:layout_marginTop="16dp" + android:layout_marginTop="10dp" android:text="@string/contacts_list_favourites_title" android:drawableEnd="@{viewModel.showFavourites ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" /> diff --git a/app/src/main/res/layout/help_fragment.xml b/app/src/main/res/layout/help_fragment.xml index 5e3f4711f6..679aa260a7 100644 --- a/app/src/main/res/layout/help_fragment.xml +++ b/app/src/main/res/layout/help_fragment.xml @@ -397,12 +397,15 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" + android:layout_marginBottom="@dimen/screen_bottom_margin" android:text="@string/help_troubleshooting_subtitle" android:textSize="14sp" android:textColor="?attr/color_main2_600" + app:layout_constraintVertical_bias="0" app:layout_constraintTop_toBottomOf="@id/debug_title" app:layout_constraintStart_toEndOf="@id/debug_icon" - app:layout_constraintEnd_toEndOf="parent" /> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent"/> diff --git a/app/src/main/res/layout/sliding_pane_separator.xml b/app/src/main/res/layout/sliding_pane_separator.xml new file mode 100644 index 0000000000..68b1bd3152 --- /dev/null +++ b/app/src/main/res/layout/sliding_pane_separator.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values-sw600dp/dimen.xml b/app/src/main/res/values-sw600dp/dimen.xml index 78da8b09d1..5edc1176fe 100644 --- a/app/src/main/res/values-sw600dp/dimen.xml +++ b/app/src/main/res/values-sw600dp/dimen.xml @@ -2,4 +2,6 @@ 150dp 600dp + 90dp + 500dp \ No newline at end of file diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 7e68dc955a..1fa8f82656 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -5,7 +5,8 @@ 10dp - 75dp + 80dp + 12sp 350dp 300dp diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b77910e78e..c57fecce3c 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -99,7 +99,7 @@ - - - + + + + + From 1e6f501deedfe87c8a6bb032dc5e55fe3c81d0b2 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 5 Sep 2025 13:50:33 +0200 Subject: [PATCH 285/593] Fixed mute mic / toggle speaker buttons background changing color when pressing the bottom bar empty space --- app/src/main/res/layout/call_actions_generic.xml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/call_actions_generic.xml b/app/src/main/res/layout/call_actions_generic.xml index 0c508bae42..0dcc1ab567 100644 --- a/app/src/main/res/layout/call_actions_generic.xml +++ b/app/src/main/res/layout/call_actions_generic.xml @@ -79,6 +79,7 @@ Date: Fri, 5 Sep 2025 15:16:56 +0200 Subject: [PATCH 286/593] Fixed missing conference subject when calling it's SIP URI without having the conference info --- .../ui/call/conference/viewmodel/ConferenceViewModel.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt index ab459f985f..6f4a1c3824 100644 --- a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt @@ -261,6 +261,7 @@ class ConferenceViewModel isPaused.postValue(!isIn) Log.i("$TAG We [${if (isIn) "are" else "aren't"}] in the conference") + subject.postValue(conference.subjectUtf8.orEmpty()) computeParticipants(false) if (conference.participantList.size >= 1) { // we do not count Log.i("$TAG Joined conference already has at least another participant") @@ -312,7 +313,7 @@ class ConferenceViewModel val chatEnabled = conference.currentParams.isChatEnabled isConversationAvailable.postValue(chatEnabled) - val confSubject = conference.subject.orEmpty() + val confSubject = conference.subjectUtf8.orEmpty() Log.i( "$TAG Configuring conference with subject [$confSubject] from call [${call.callLog.callId}]" ) From 4639e054bbd1d5dd095bd2c3230a23d45cf4cd18 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 5 Sep 2025 16:22:34 +0200 Subject: [PATCH 287/593] Ask CallActivity to finish if no call found when trying to answer/hangup --- .../org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index b8ccb116db..169c1a66bc 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -617,6 +617,7 @@ class CurrentCallViewModel coreContext.answerCall(call) } else { Log.e("$TAG No call found in incoming state, can't answer any!") + finishActivityEvent.postValue(Event(true)) } } } @@ -628,6 +629,9 @@ class CurrentCallViewModel Log.i("$TAG Terminating call [${currentCall.remoteAddress.asStringUriOnly()}]") terminatedByUser = true coreContext.terminateCall(currentCall) + } else { + Log.e("$TAG No call to decline!") + finishActivityEvent.postValue(Event(true)) } } } From 7fdbaf5fd6530a497566043773f8f6e59d72db24 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 11 Sep 2025 09:14:11 +0200 Subject: [PATCH 288/593] Updated dependencies --- gradle/libs.versions.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ed5cbead9b..5e177ebc93 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ firebaseBomVersion = "34.2.0" ktlint = "12.3.0" annotations = "1.9.1" -activity = "1.10.1" +activity = "1.11.0" appcompat = "1.7.1" constraintLayout = "2.2.1" coreKtx = "1.17.0" @@ -21,11 +21,11 @@ slidingpanelayout = "1.2.0" window = "1.4.0" gridlayout = "1.1.0" securityCryptoKtx = "1.1.0" -navigation = "2.9.3" -emoji2 = "1.5.0" +navigation = "2.9.4" +emoji2 = "1.6.0" car = "1.7.0" flexbox = "3.0.0" -material = "1.12.0" +material = "1.13.0" #noinspection NewerVersionAvailable protobuf = "3.25.5" coil = "3.3.0" From 6f1439756eba26ca37c9c4c4bd170de9f74512fb Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 11 Sep 2025 11:13:45 +0200 Subject: [PATCH 289/593] Improved bodyless friendlist presence received processing --- .../main/java/org/linphone/contacts/ContactsManager.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 179166db89..efae440f3c 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -160,19 +160,25 @@ class ContactsManager if (friendList.isSubscriptionBodyless) { Log.i("$TAG Bodyless friendlist [${friendList.displayName}] presence received") + var atLeastOneFriendAdded = false for (friend in friends) { if (friend != null) { val address = friend.address if (address != null) { - Log.d( + Log.i( "$TAG Newly discovered SIP Address [${address.asStringUriOnly()}] for friend [${friend.name}] in bodyless list [${friendList.displayName}]" ) newContactAddedWithSipUri(friend, address) + atLeastOneFriendAdded = true } } } - notifyContactsListChanged() + if (atLeastOneFriendAdded) { + notifyContactsListChanged() + } else { + Log.w("$TAG No new friend detected in the received bodyless friendlist, not refreshing contacts in app") + } } } From 719b28f0ab9236ff9c284ff3bf2c50da84f747eb Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 12 Sep 2025 14:59:47 +0200 Subject: [PATCH 290/593] Fixed account labelled as Disabled instead of Disconnected if network isn't reachable --- .../linphone/ui/main/model/AccountModel.kt | 82 ++++++++++++------- .../viewmodel/AccountProfileViewModel.kt | 7 +- app/src/main/res/layout/account_list_cell.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 65 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/model/AccountModel.kt b/app/src/main/java/org/linphone/ui/main/model/AccountModel.kt index 1d030e215d..65100beb57 100644 --- a/app/src/main/java/org/linphone/ui/main/model/AccountModel.kt +++ b/app/src/main/java/org/linphone/ui/main/model/AccountModel.kt @@ -184,33 +184,29 @@ class AccountModel } @WorkerThread - private fun update() { - Log.i( - "$TAG Refreshing info for account [${account.params.identityAddress?.asStringUriOnly()}]" - ) - - trust.postValue(SecurityLevel.EndToEndEncryptedAndVerified) - showTrust.postValue(isEndToEndEncryptionMandatory()) - - val name = LinphoneUtils.getDisplayName(account.params.identityAddress) - displayName.postValue(name) - - initials.postValue(AppUtils.getInitials(name)) + fun computeNotificationsCount() { + notificationsCount.postValue(account.unreadChatMessageCount + account.missedCallsCount) + } - val pictureUri = account.params.pictureUri.orEmpty() - if (pictureUri != picturePath.value.orEmpty()) { - picturePath.postValue(pictureUri) - Log.d("$TAG Account picture URI is [$pictureUri]") + @WorkerThread + fun updateRegistrationState() { + val state = if (account.state == RegistrationState.None) { + // If the account has been disabled manually, use the Cleared status instead of None + if (!account.params.isRegisterEnabled) { + Log.w( + "$TAG Account real registration state is None but using Cleared instead as it was manually disabled by the user" + ) + RegistrationState.Cleared + } else { + account.state + } + } else { + account.state } - - isDefault.postValue(coreContext.core.defaultAccount == account) - computeNotificationsCount() - - val state = account.state registrationState.postValue(state) val label = when (state) { - RegistrationState.None, RegistrationState.Cleared -> { + RegistrationState.Cleared -> { AppUtils.getString( R.string.drawer_menu_account_connection_status_cleared ) @@ -228,15 +224,21 @@ class AccountModel R.string.drawer_menu_account_connection_status_connected ) } + RegistrationState.None -> { + AppUtils.getString( + R.string.drawer_menu_account_connection_status_disconnected + ) + } RegistrationState.Refreshing -> AppUtils.getString( R.string.drawer_menu_account_connection_status_refreshing ) - else -> "${account.state}" + else -> "$state" } registrationStateLabel.postValue(label) + Log.i("$TAG Account registration state is [$state]") - val summary = when (account.state) { - RegistrationState.None, RegistrationState.Cleared -> AppUtils.getString( + val summary = when (state) { + RegistrationState.Cleared -> AppUtils.getString( R.string.manage_account_status_cleared_summary ) RegistrationState.Refreshing, RegistrationState.Progress -> AppUtils.getString( @@ -248,14 +250,38 @@ class AccountModel RegistrationState.Ok -> AppUtils.getString( R.string.manage_account_status_connected_summary ) - else -> "${account.state}" + RegistrationState.None -> AppUtils.getString( + R.string.manage_account_status_disconnected_summary + ) + else -> "$state" } registrationStateSummary.postValue(summary) } @WorkerThread - fun computeNotificationsCount() { - notificationsCount.postValue(account.unreadChatMessageCount + account.missedCallsCount) + private fun update() { + Log.i( + "$TAG Refreshing info for account [${account.params.identityAddress?.asStringUriOnly()}]" + ) + + trust.postValue(SecurityLevel.EndToEndEncryptedAndVerified) + showTrust.postValue(isEndToEndEncryptionMandatory()) + + val name = LinphoneUtils.getDisplayName(account.params.identityAddress) + displayName.postValue(name) + + initials.postValue(AppUtils.getInitials(name)) + + val pictureUri = account.params.pictureUri.orEmpty() + if (pictureUri != picturePath.value.orEmpty()) { + picturePath.postValue(pictureUri) + Log.d("$TAG Account picture URI is [$pictureUri]") + } + + isDefault.postValue(coreContext.core.defaultAccount == account) + computeNotificationsCount() + + updateRegistrationState() } } diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt index f11c396565..6f1159abc8 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt @@ -378,7 +378,7 @@ class AccountProfileViewModel @UiThread fun toggleRegister() { - coreContext.postOnCoreThread { + coreContext.postOnCoreThread { core -> val params = account.params val copy = params.clone() copy.isRegisterEnabled = !params.isRegisterEnabled @@ -387,6 +387,11 @@ class AccountProfileViewModel ) account.params = copy registerEnabled.postValue(account.params.isRegisterEnabled) + + if (!core.isNetworkReachable) { + // To reflect the difference between Disabled & Disconnected + accountModel.value?.updateRegistrationState() + } } } diff --git a/app/src/main/res/layout/account_list_cell.xml b/app/src/main/res/layout/account_list_cell.xml index 4b30071c25..47614770e9 100644 --- a/app/src/main/res/layout/account_list_cell.xml +++ b/app/src/main/res/layout/account_list_cell.xml @@ -71,7 +71,7 @@ android:background="@drawable/shape_squircle_main2_200_background" android:gravity="center" android:text="@{model.registrationStateLabel, default=@string/drawer_menu_account_connection_status_connected}" - android:textColor="@{model.registrationState == RegistrationState.Ok ? @color/success_500 : model.registrationState == RegistrationState.Failed ? @color/danger_500 : model.registrationState == RegistrationState.Cleared || model.registrationState == RegistrationState.None ? @color/warning_600 : @color/main2_500, default=@color/success_500}" + android:textColor="@{model.registrationState == RegistrationState.Ok ? @color/success_500 : model.registrationState == RegistrationState.Failed ? @color/danger_500 : model.registrationState == RegistrationState.Cleared ? @color/warning_600 : @color/main2_500, default=@color/success_500}" android:textSize="12sp" app:layout_constraintHorizontal_bias="0" app:layout_constraintStart_toStartOf="@id/name" diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 28956bcd1f..ac7236c06b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -151,6 +151,7 @@ Mon compte Connecté + Déconnecté Rafraîchissement… Désactivé Connexion… @@ -331,6 +332,7 @@ Modifier Supprimer Vous êtes en ligne, on peut vous joindre. + Vous êtes hors ligne, probablement car vous n\'êtes pas actuellement connecté à internet. Compte désactivé, vous ne recevrez ni appel ni message. Connexion en cours, merci de patienter… Erreur de connexion, vérifiez vos paramètres. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4b5d61d4da..d752f78b4c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -193,6 +193,7 @@ Manage the profile Connected + Disconnected Refreshing Disabled Connecting… @@ -373,6 +374,7 @@ Edit picture Remove picture This account in online, everybody can call you. + This account in offline, probably because you aren\'t connected to internet right now. Account has been disabled, you won\'t receive any call or message. Account is connecting to the server, please wait… Account connection failed, check your settings. From 5f17dd8534c31a0b816008c1ad3a91c3bdb2aa44 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 15 Sep 2025 09:28:15 +0200 Subject: [PATCH 291/593] Added menu icon in top bar next to current profile avatar + fixed layout icon while in conference --- app/src/main/res/drawable/layout.xml | 9 ++++++++ app/src/main/res/drawable/list.xml | 9 ++++++++ app/src/main/res/drawable/notebook.xml | 9 -------- .../call_conference_actions_bottom_sheet.xml | 2 +- .../res/layout-land/main_activity_top_bar.xml | 23 +++++++++++++------ .../main_activity_top_bar.xml | 20 +++++++++++++--- .../call_conference_actions_bottom_sheet.xml | 2 +- .../main/res/layout/main_activity_top_bar.xml | 23 +++++++++++++------ 8 files changed, 69 insertions(+), 28 deletions(-) create mode 100644 app/src/main/res/drawable/layout.xml create mode 100644 app/src/main/res/drawable/list.xml delete mode 100644 app/src/main/res/drawable/notebook.xml diff --git a/app/src/main/res/drawable/layout.xml b/app/src/main/res/drawable/layout.xml new file mode 100644 index 0000000000..489e4c2e33 --- /dev/null +++ b/app/src/main/res/drawable/layout.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/list.xml b/app/src/main/res/drawable/list.xml new file mode 100644 index 0000000000..4a35bedf51 --- /dev/null +++ b/app/src/main/res/drawable/list.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/notebook.xml b/app/src/main/res/drawable/notebook.xml deleted file mode 100644 index 826e462747..0000000000 --- a/app/src/main/res/drawable/notebook.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/layout-land/call_conference_actions_bottom_sheet.xml b/app/src/main/res/layout-land/call_conference_actions_bottom_sheet.xml index 9d02d3e611..39401ac971 100644 --- a/app/src/main/res/layout-land/call_conference_actions_bottom_sheet.xml +++ b/app/src/main/res/layout-land/call_conference_actions_bottom_sheet.xml @@ -107,7 +107,7 @@ android:layout_marginTop="@dimen/call_extra_button_top_margin" android:padding="@dimen/call_button_icon_padding" android:background="@drawable/in_call_button_background_red" - android:src="@drawable/notebook" + android:src="@drawable/layout" android:contentDescription="@string/call_action_change_layout" app:tint="@color/in_call_button_tint_color" app:layout_constraintDimensionRatio="1:1" diff --git a/app/src/main/res/layout-land/main_activity_top_bar.xml b/app/src/main/res/layout-land/main_activity_top_bar.xml index 0892801e3f..4bc98dfc9a 100644 --- a/app/src/main/res/layout-land/main_activity_top_bar.xml +++ b/app/src/main/res/layout-land/main_activity_top_bar.xml @@ -30,7 +30,7 @@ + + @@ -80,7 +93,6 @@ android:onClick="@{() -> viewModel.openSearchBar()}" android:layout_width="wrap_content" android:layout_height="0dp" - android:layout_marginEnd="5dp" android:src="@drawable/magnifying_glass" android:contentDescription="@string/content_description_open_filter" app:layout_constraintDimensionRatio="1:1" @@ -95,7 +107,6 @@ android:onClick="@{extraActionClickListener}" android:layout_width="wrap_content" android:layout_height="0dp" - android:layout_marginEnd="5dp" android:src="@{extraActionIcon, default=@drawable/dots_three_vertical}" android:contentDescription="@{extraActionContentDescription}" android:visibility="@{!viewModel.searchBarVisible && enableExtraAction ? View.VISIBLE : View.GONE, default=gone}" @@ -111,7 +122,6 @@ android:onClick="@{() -> viewModel.closeSearchBar()}" android:layout_width="wrap_content" android:layout_height="0dp" - android:layout_marginStart="5dp" android:src="@drawable/caret_left" android:contentDescription="@string/content_description_cancel_filter" app:layout_constraintDimensionRatio="1:1" @@ -158,7 +168,6 @@ android:onClick="@{() -> viewModel.clearFilter()}" android:layout_width="wrap_content" android:layout_height="0dp" - android:layout_marginEnd="5dp" android:src="@drawable/x" android:contentDescription="@string/content_description_clear_filter" app:layout_constraintDimensionRatio="1:1" diff --git a/app/src/main/res/layout-sw600dp-land/main_activity_top_bar.xml b/app/src/main/res/layout-sw600dp-land/main_activity_top_bar.xml index 3345c9f5a8..397ea60285 100644 --- a/app/src/main/res/layout-sw600dp-land/main_activity_top_bar.xml +++ b/app/src/main/res/layout-sw600dp-land/main_activity_top_bar.xml @@ -29,7 +29,7 @@ + + diff --git a/app/src/main/res/layout/call_conference_actions_bottom_sheet.xml b/app/src/main/res/layout/call_conference_actions_bottom_sheet.xml index 13a30ab0c6..e8af291681 100644 --- a/app/src/main/res/layout/call_conference_actions_bottom_sheet.xml +++ b/app/src/main/res/layout/call_conference_actions_bottom_sheet.xml @@ -107,7 +107,7 @@ android:layout_marginTop="@dimen/call_extra_button_top_margin" android:padding="@dimen/call_button_icon_padding" android:background="@drawable/in_call_button_background_red" - android:src="@drawable/notebook" + android:src="@drawable/layout" android:contentDescription="@string/call_action_change_layout" app:tint="@color/in_call_button_tint_color" app:layout_constraintDimensionRatio="1:1" diff --git a/app/src/main/res/layout/main_activity_top_bar.xml b/app/src/main/res/layout/main_activity_top_bar.xml index 1f3a3689ce..895e558cb3 100644 --- a/app/src/main/res/layout/main_activity_top_bar.xml +++ b/app/src/main/res/layout/main_activity_top_bar.xml @@ -30,7 +30,7 @@ + + @@ -80,7 +93,6 @@ android:onClick="@{() -> viewModel.openSearchBar()}" android:layout_width="wrap_content" android:layout_height="0dp" - android:layout_marginEnd="5dp" android:src="@drawable/magnifying_glass" android:contentDescription="@string/content_description_open_filter" app:layout_constraintDimensionRatio="1:1" @@ -95,7 +107,6 @@ android:onClick="@{extraActionClickListener}" android:layout_width="wrap_content" android:layout_height="0dp" - android:layout_marginEnd="5dp" android:src="@{extraActionIcon, default=@drawable/dots_three_vertical}" android:contentDescription="@{extraActionContentDescription}" android:visibility="@{!viewModel.searchBarVisible && enableExtraAction ? View.VISIBLE : View.GONE, default=gone}" @@ -111,7 +122,6 @@ android:onClick="@{() -> viewModel.closeSearchBar()}" android:layout_width="wrap_content" android:layout_height="0dp" - android:layout_marginStart="5dp" android:src="@drawable/caret_left" android:contentDescription="@string/content_description_cancel_filter" app:layout_constraintDimensionRatio="1:1" @@ -158,7 +168,6 @@ android:onClick="@{() -> viewModel.clearFilter()}" android:layout_width="wrap_content" android:layout_height="0dp" - android:layout_marginEnd="5dp" android:src="@drawable/x" android:contentDescription="@string/content_description_clear_filter" app:layout_constraintDimensionRatio="1:1" From 9d3ef9e8a5a182db30d583e4838a94573b35d1c0 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 15 Sep 2025 11:26:30 +0200 Subject: [PATCH 292/593] Fix for empty fragment still opened after device rotation if user clicked on the empty part --- .../org/linphone/ui/main/fragment/EmptyFragment.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/fragment/EmptyFragment.kt b/app/src/main/java/org/linphone/ui/main/fragment/EmptyFragment.kt index dc66a32682..bc2d195368 100644 --- a/app/src/main/java/org/linphone/ui/main/fragment/EmptyFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/fragment/EmptyFragment.kt @@ -24,14 +24,19 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.UiThread +import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import org.linphone.databinding.EmptyFragmentBinding import org.linphone.ui.GenericFragment +import org.linphone.ui.main.viewmodel.SharedMainViewModel +import org.linphone.utils.Event @UiThread class EmptyFragment : GenericFragment() { private lateinit var binding: EmptyFragmentBinding + protected lateinit var sharedViewModel: SharedMainViewModel + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -45,11 +50,19 @@ class EmptyFragment : GenericFragment() { super.onViewCreated(view, savedInstanceState) binding.lifecycleOwner = viewLifecycleOwner + + sharedViewModel = requireActivity().run { + ViewModelProvider(this)[SharedMainViewModel::class.java] + } } override fun onResume() { super.onResume() findNavController().popBackStack() + + // This should prevent empty fragment from staying visible + // after the device rotated if user touched the empty fragment on the right + sharedViewModel.closeSlidingPaneEvent.postValue(Event(true)) } } From 2ce07b5e8907cf0afc2b7393da67422b15f56bd9 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 15 Sep 2025 14:22:44 +0200 Subject: [PATCH 293/593] Updated CHANGELOG & version code from release/6.0 branch --- CHANGELOG.md | 16 ++++++++++++++++ app/build.gradle.kts | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd0911540c..398ac30422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,22 @@ Group changes to describe their impact on the project, as follows: - Reworked some settings (moved calls related ones from advanced settings to advanced calls settings) - Increased shared media preview size in chat - Un-encrypted conversation warning will be more visible for accounts that support end-to-end encrypted conversations +- Made numpad buttons larger by changing their shape + +## [6.0.18] - 2025-09-15 + +### Added +- Added menu icon next to currently selected account avatar to make the drawer menu easier to understand +- Added missing dialpad floating action button in the call transfer fragment + +### Changed +- Improved bodyless friendlist presence process when it's received + +### Fixed +- Fixed "End-to-end encrypted call" label while in conference, the call may be end-to-end encrypted but only to the conference server, not to all participants +- Fixed missing meeting subject when calling the conference SIP URI if the conference info doesn't exist yet +- Finish CallActivity if no call is found when trying to answer/decline a call from the IncomingCallFragment +- Prevent empty screen when rotating the device and clicking on the empty part next to the list while in landscape and then rotating the device back to portrait ## [6.0.17] - 2025-09-02 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4a39a720c6..23e6b4f478 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -100,7 +100,7 @@ android { applicationId = packageName minSdk = 28 targetSdk = 36 - versionCode = 600017 // 6.00.017 + versionCode = 600018 // 6.00.018 versionName = "6.1.0-alpha" manifestPlaceholders["appAuthRedirectScheme"] = packageName From 99936e8f753a13e4c5a7a885f136399af06bf93e Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 17 Sep 2025 09:18:50 +0200 Subject: [PATCH 294/593] Removed font padding on main fragments' titles --- app/src/main/res/layout-land/main_activity_top_bar.xml | 1 + app/src/main/res/layout-sw600dp-land/main_activity_top_bar.xml | 2 +- app/src/main/res/layout/main_activity_top_bar.xml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout-land/main_activity_top_bar.xml b/app/src/main/res/layout-land/main_activity_top_bar.xml index 4bc98dfc9a..87418db2d7 100644 --- a/app/src/main/res/layout-land/main_activity_top_bar.xml +++ b/app/src/main/res/layout-land/main_activity_top_bar.xml @@ -82,6 +82,7 @@ android:text="@{viewModel.title, default=`Title`}" android:textColor="?attr/color_on_main" android:textSize="20sp" + android:includeFontPadding="false" app:layout_constraintEnd_toStartOf="@id/search_toggle" app:layout_constraintStart_toEndOf="@id/avatar" app:layout_constraintTop_toTopOf="@id/avatar" diff --git a/app/src/main/res/layout-sw600dp-land/main_activity_top_bar.xml b/app/src/main/res/layout-sw600dp-land/main_activity_top_bar.xml index 397ea60285..c369b74038 100644 --- a/app/src/main/res/layout-sw600dp-land/main_activity_top_bar.xml +++ b/app/src/main/res/layout-sw600dp-land/main_activity_top_bar.xml @@ -64,12 +64,12 @@ android:id="@+id/title" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginStart="10dp" android:layout_marginEnd="10dp" android:gravity="center_vertical" android:text="@{viewModel.title, default=`Title`}" android:textColor="?attr/color_text" android:textSize="28sp" + android:includeFontPadding="false" app:layout_constraintEnd_toStartOf="@id/search_toggle" app:layout_constraintStart_toEndOf="@id/drawer_menu" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/main_activity_top_bar.xml b/app/src/main/res/layout/main_activity_top_bar.xml index 895e558cb3..f2f85bfa24 100644 --- a/app/src/main/res/layout/main_activity_top_bar.xml +++ b/app/src/main/res/layout/main_activity_top_bar.xml @@ -82,6 +82,7 @@ android:text="@{viewModel.title, default=`Title`}" android:textColor="?attr/color_on_main" android:textSize="20sp" + android:includeFontPadding="false" app:layout_constraintEnd_toStartOf="@id/search_toggle" app:layout_constraintStart_toEndOf="@id/avatar" app:layout_constraintTop_toTopOf="@id/avatar" From 808dc92cd76b406cd0b9656b0903910ba1b11b04 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 17 Sep 2025 12:17:06 +0200 Subject: [PATCH 295/593] Added PDF file preview in conversation (message bubble + documents list) --- CHANGELOG.md | 1 + .../linphone/ui/main/chat/model/FileModel.kt | 78 +++++++++++++++---- .../chat_bubble_single_file_content.xml | 15 ++++ 3 files changed, 81 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 398ac30422..bab4b102cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Group changes to describe their impact on the project, as follows: ### Added - Added the ability to edit/delete chat messages sent less than 24 hours ago. +- Added PDF preview in conversation (message bubble & documents list) - Added hover effect when using a mouse (useful for tablets or devices with desktop mode) - Support right click on some items to open bottom sheet/menu - Added toggle speaker action in active call notification diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt index 1b4c2d315c..2d922731e0 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt @@ -19,9 +19,11 @@ */ package org.linphone.ui.main.chat.model +import android.graphics.pdf.PdfRenderer import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION import android.media.ThumbnailUtils +import android.os.ParcelFileDescriptor import android.provider.MediaStore import androidx.annotation.AnyThread import androidx.annotation.UiThread @@ -35,6 +37,9 @@ import org.linphone.core.tools.Log import org.linphone.utils.FileUtils import org.linphone.utils.TimestampUtils import androidx.core.net.toUri +import androidx.core.graphics.createBitmap +import kotlinx.coroutines.withContext +import java.io.File class FileModel @AnyThread @@ -97,6 +102,9 @@ class FileModel if (!isWaitingToBeDownloaded) { val extension = FileUtils.getExtensionFromFileName(path) isPdf = extension == "pdf" + if (isPdf) { + loadPdfPreview() + } val mime = FileUtils.getMimeTypeFromExtension(extension) mimeTypeString = mime @@ -167,22 +175,66 @@ class FileModel FileUtils.deleteFile(path) } + @AnyThread + private fun loadPdfPreview() { + scope.launch { + withContext(Dispatchers.IO) { + try { + val pdfFileDescriptor = ParcelFileDescriptor.open( + File(path), + ParcelFileDescriptor.MODE_READ_ONLY + ) + if (pdfFileDescriptor == null) { + Log.e("$TAG Failed to get a file descriptor for PDF at [$path]") + return@withContext + } + + val pdfRenderer = PdfRenderer(pdfFileDescriptor) + val pdfFirstPage = pdfRenderer.openPage(0) + val previewBitmap = createBitmap(pdfFirstPage.width, pdfFirstPage.height) + pdfFirstPage.render( + previewBitmap, + null, + null, + PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY + ) + + val previewPath = FileUtils.storeBitmap(previewBitmap, fileName) + Log.i("$TAG Preview of PDF file [$path] available at [$previewPath]") + mediaPreview.postValue(previewPath) + mediaPreviewAvailable.postValue(true) + + previewBitmap.recycle() + pdfFirstPage.close() + pdfRenderer.close() + pdfFileDescriptor.close() + } catch (e: Exception) { + Log.e("$TAG Failed to get image preview for PDF file [$path]: $e") + } + } + } + } + @AnyThread private fun loadVideoPreview() { - try { - Log.i("$TAG Try to create an image preview of video file [$path]") - val previewBitmap = ThumbnailUtils.createVideoThumbnail( - path, - MediaStore.Images.Thumbnails.MINI_KIND - ) - if (previewBitmap != null) { - val previewPath = FileUtils.storeBitmap(previewBitmap, fileName) - Log.i("$TAG Preview of video file [$path] available at [$previewPath]") - mediaPreview.postValue(previewPath) - mediaPreviewAvailable.postValue(true) + scope.launch { + withContext(Dispatchers.IO) { + try { + Log.i("$TAG Try to create an image preview of video file [$path]") + val previewBitmap = ThumbnailUtils.createVideoThumbnail( + path, + MediaStore.Images.Thumbnails.MINI_KIND + ) + if (previewBitmap != null) { + val previewPath = FileUtils.storeBitmap(previewBitmap, fileName) + Log.i("$TAG Preview of video file [$path] available at [$previewPath]") + mediaPreview.postValue(previewPath) + mediaPreviewAvailable.postValue(true) + } + } catch (e: Exception) { + Log.e("$TAG Failed to get image preview for file [$path]: $e") + } } - } catch (e: Exception) { - Log.e("$TAG Failed to get image preview for file [$path]: $e") } } diff --git a/app/src/main/res/layout/chat_bubble_single_file_content.xml b/app/src/main/res/layout/chat_bubble_single_file_content.xml index 3a94ab5023..221df88771 100644 --- a/app/src/main/res/layout/chat_bubble_single_file_content.xml +++ b/app/src/main/res/layout/chat_bubble_single_file_content.xml @@ -64,6 +64,21 @@ app:layout_constraintEnd_toEndOf="@id/left_background" app:tint="?attr/color_main2_600" /> + + Date: Wed, 17 Sep 2025 13:36:15 +0200 Subject: [PATCH 296/593] Prevent app crash when trying to open a corrupted PDF sent/received by chat --- .../org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt | 7 ++++++- app/src/main/res/values-fr/strings.xml | 3 ++- app/src/main/res/values/strings.xml | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt index a623ee8b39..b35c042e60 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt @@ -291,7 +291,12 @@ class FileViewModel Log.e("$TAG Can't open PDF, probably protected by a password: $se") pdfCurrentPage.postValue("0") pdfPages.postValue("0") - showRedToast(R.string.conversation_pdf_file_cant_be_opened_error_toast, R.drawable.warning_circle) + showRedToast(R.string.conversation_pdf_password_protected_file_cant_be_opened_error_toast, R.drawable.warning_circle) + } catch (e: Exception) { + Log.e("$TAG Can't open PDF, it may be corrupted: $e") + pdfCurrentPage.postValue("0") + pdfPages.postValue("0") + showRedToast(R.string.conversation_pdf_file_error_toast, R.drawable.warning_circle) } } } diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ac7236c06b..40130bbcc8 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -552,7 +552,8 @@ Ouvrir la galerie Choisir un fichier Impossible d\'ouvrir le fichier ! - Impossible d\'ouvrir un PDF protégé par mot de passe + Impossible d\'ouvrir un PDF protégé par mot de passe + Impossible d\'ouvrir ce PDF, le fichier est peut-être corrompu Modification du message Modifié Supprimer le message ? diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d752f78b4c..f3ae00480d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -595,7 +595,8 @@ Open gallery Pick file File can\'t be opened! - Can\'t open password protected PDFs yet + Can\'t open password protected PDFs yet + Can\'t open this PDF, file may be corrupted Message being edited Edited Delete this message? From ae7a3c5bce18e307c6a8410f876adf75f6a75383 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 16 Sep 2025 17:15:56 +0200 Subject: [PATCH 297/593] Load contents by chunks instead of loading all of them at once --- CHANGELOG.md | 1 + ...tener.kt => RecyclerViewScrollListener.kt} | 44 +++++++++----- .../ConversationDocumentsListFragment.kt | 37 ++++++++++++ .../chat/fragment/ConversationFragment.kt | 6 +- .../fragment/ConversationMediaListFragment.kt | 37 ++++++++++++ .../ConversationDocumentsListViewModel.kt | 58 +++++++++++++++---- .../ConversationMediaListViewModel.kt | 58 +++++++++++++++---- 7 files changed, 200 insertions(+), 41 deletions(-) rename app/src/main/java/org/linphone/ui/main/chat/{ConversationScrollListener.kt => RecyclerViewScrollListener.kt} (65%) diff --git a/CHANGELOG.md b/CHANGELOG.md index bab4b102cf..6accff7adb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Group changes to describe their impact on the project, as follows: ### Changed - Hide SIP address/phone number picker dialog if contact has exactly one SIP address matching both the app default domain & the currently selected account domain - Improved UI on tablets with screen sw600dp and higher, will look more like our desktop app +- Now loading media/documents contents in conversation by chunks (instead of all of them at once) - Simplified audio device name in settings - Reworked some settings (moved calls related ones from advanced settings to advanced calls settings) - Increased shared media preview size in chat diff --git a/app/src/main/java/org/linphone/ui/main/chat/ConversationScrollListener.kt b/app/src/main/java/org/linphone/ui/main/chat/RecyclerViewScrollListener.kt similarity index 65% rename from app/src/main/java/org/linphone/ui/main/chat/ConversationScrollListener.kt rename to app/src/main/java/org/linphone/ui/main/chat/RecyclerViewScrollListener.kt index edf0b60973..bc7a8f8832 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/ConversationScrollListener.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/RecyclerViewScrollListener.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2020 Belledonne Communications SARL. + * Copyright (c) 2010-2025 Belledonne Communications SARL. * * This file is part of linphone-android * (see https://www.linphone.org). @@ -21,13 +21,12 @@ package org.linphone.ui.main.chat import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import org.linphone.core.tools.Log -internal abstract class ConversationScrollListener(private val mLayoutManager: LinearLayoutManager) : +internal abstract class RecyclerViewScrollListener(private val layoutManager: LinearLayoutManager, private val visibleThreshold: Int, private val scrollingTopToBottom: Boolean) : RecyclerView.OnScrollListener() { companion object { - // The minimum amount of items to have below your current scroll position - // before loading more. - private const val VISIBLE_THRESHOLD = 5 + private const val TAG = "[RecyclerView Scroll Listener]" } // The total number of items in the data set after the last load @@ -40,9 +39,9 @@ internal abstract class ConversationScrollListener(private val mLayoutManager: L // We are given a few useful parameters to help us work out if we need to load some more data, // but first we check if we are waiting for the previous load to finish. override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { - val totalItemCount = mLayoutManager.itemCount - val firstVisibleItemPosition: Int = mLayoutManager.findFirstVisibleItemPosition() - val lastVisibleItemPosition: Int = mLayoutManager.findLastVisibleItemPosition() + val totalItemCount = layoutManager.itemCount + val firstVisibleItemPosition: Int = layoutManager.findFirstVisibleItemPosition() + val lastVisibleItemPosition: Int = layoutManager.findLastVisibleItemPosition() // If the total item count is zero and the previous isn't, assume the // list is invalidated and should be reset back to initial state @@ -64,21 +63,34 @@ internal abstract class ConversationScrollListener(private val mLayoutManager: L val userHasScrolledUp = lastVisibleItemPosition != totalItemCount - 1 if (userHasScrolledUp) { onScrolledUp() + Log.d("$TAG Scrolled up") } else { onScrolledToEnd() + Log.d("$TAG Scrolled to end") } // If it isn’t currently loading, we check to see if we have breached - // the mVisibleThreshold and need to reload more data. + // the visibleThreshold and need to reload more data. // If we do need to reload some more data, we execute onLoadMore to fetch the data. // threshold should reflect how many total columns there are too - if (!loading && - firstVisibleItemPosition < VISIBLE_THRESHOLD && - firstVisibleItemPosition >= 0 && - lastVisibleItemPosition < totalItemCount - VISIBLE_THRESHOLD - ) { - onLoadMore(totalItemCount) - loading = true + if (!loading) { + if (scrollingTopToBottom) { + if (lastVisibleItemPosition >= totalItemCount - visibleThreshold) { + Log.d( + "$TAG Last visible item position [$lastVisibleItemPosition] reached [${totalItemCount - visibleThreshold}], loading more (current total items is [$totalItemCount])" + ) + loading = true + onLoadMore(totalItemCount) + } + } else { + if (firstVisibleItemPosition < visibleThreshold) { + Log.d( + "$TAG First visible item position [$firstVisibleItemPosition] < visibleThreshold [$visibleThreshold], loading more (current total items is [$totalItemCount])" + ) + loading = true + onLoadMore(totalItemCount) + } + } } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt index d908c1f142..028383a8eb 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt @@ -33,6 +33,7 @@ import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import org.linphone.core.tools.Log import org.linphone.databinding.ChatDocumentsFragmentBinding +import org.linphone.ui.main.chat.RecyclerViewScrollListener import org.linphone.ui.main.chat.adapter.ConversationsFilesAdapter import org.linphone.ui.main.chat.model.FileModel import org.linphone.ui.main.chat.viewmodel.ConversationDocumentsListViewModel @@ -57,6 +58,8 @@ class ConversationDocumentsListFragment : SlidingPaneChildFragment() { private val args: ConversationMediaListFragmentArgs by navArgs() + private lateinit var scrollListener: RecyclerViewScrollListener + override fun goBack(): Boolean { try { return findNavController().popBackStack() @@ -130,6 +133,40 @@ class ConversationDocumentsListFragment : SlidingPaneChildFragment() { goToFileViewer(model) } } + + scrollListener = object : RecyclerViewScrollListener(layoutManager, 4, true) { + @UiThread + override fun onLoadMore(totalItemsCount: Int) { + Log.i("$TAG Asking for more data to display, currently displayed items count is [$totalItemsCount]") + viewModel.loadMoreData(totalItemsCount) + } + + @UiThread + override fun onScrolledUp() { + + } + + @UiThread + override fun onScrolledToEnd() { + + } + } + } + + override fun onResume() { + super.onResume() + + if (::scrollListener.isInitialized) { + binding.documentsList.addOnScrollListener(scrollListener) + } + } + + override fun onPause() { + super.onPause() + + if (::scrollListener.isInitialized) { + binding.documentsList.removeOnScrollListener(scrollListener) + } } private fun goToFileViewer(fileModel: FileModel) { diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 4f68d71cdf..8e06bd6685 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -69,7 +69,7 @@ import org.linphone.core.tools.Log import org.linphone.databinding.ChatConversationFragmentBinding import org.linphone.databinding.ChatConversationPopupMenuBinding import org.linphone.ui.GenericActivity -import org.linphone.ui.main.chat.ConversationScrollListener +import org.linphone.ui.main.chat.RecyclerViewScrollListener import org.linphone.ui.main.chat.adapter.ConversationEventAdapter import org.linphone.ui.main.chat.adapter.MessageBottomSheetAdapter import org.linphone.ui.main.chat.model.FileModel @@ -298,7 +298,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { } } - private lateinit var scrollListener: ConversationScrollListener + private lateinit var scrollListener: RecyclerViewScrollListener private lateinit var headerItemDecoration: RecyclerViewHeaderDecoration @@ -1005,7 +1005,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { binding.sendArea.messageToSend.addTextChangedListener(textObserver) - scrollListener = object : ConversationScrollListener(layoutManager) { + scrollListener = object : RecyclerViewScrollListener(layoutManager, 5, false) { @UiThread override fun onLoadMore(totalItemsCount: Int) { if (viewModel.searchInProgress.value == false) { diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt index 0f7c6a307a..4a88e1930f 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt @@ -36,6 +36,7 @@ import org.linphone.R import org.linphone.core.tools.Log import org.linphone.databinding.ChatMediaFragmentBinding import org.linphone.ui.GenericActivity +import org.linphone.ui.main.chat.RecyclerViewScrollListener import org.linphone.ui.main.chat.adapter.ConversationsFilesAdapter import org.linphone.ui.main.chat.model.FileModel import org.linphone.ui.main.chat.viewmodel.ConversationMediaListViewModel @@ -58,6 +59,8 @@ class ConversationMediaListFragment : SlidingPaneChildFragment() { private val args: ConversationMediaListFragmentArgs by navArgs() + private lateinit var scrollListener: RecyclerViewScrollListener + override fun goBack(): Boolean { try { return findNavController().popBackStack() @@ -159,6 +162,40 @@ class ConversationMediaListFragment : SlidingPaneChildFragment() { goToFileViewer(model) } } + + scrollListener = object : RecyclerViewScrollListener(layoutManager, spanCount, true) { + @UiThread + override fun onLoadMore(totalItemsCount: Int) { + Log.i("$TAG Asking for more data to display, currently displayed items count is [$totalItemsCount]") + viewModel.loadMoreData(totalItemsCount) + } + + @UiThread + override fun onScrolledUp() { + + } + + @UiThread + override fun onScrolledToEnd() { + + } + } + } + + override fun onResume() { + super.onResume() + + if (::scrollListener.isInitialized) { + binding.mediaList.addOnScrollListener(scrollListener) + } + } + + override fun onPause() { + super.onPause() + + if (::scrollListener.isInitialized) { + binding.mediaList.removeOnScrollListener(scrollListener) + } } private fun goToFileViewer(fileModel: FileModel) { diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationDocumentsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationDocumentsListViewModel.kt index 82ceaab0bc..7d85805eed 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationDocumentsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationDocumentsListViewModel.kt @@ -22,16 +22,21 @@ package org.linphone.ui.main.chat.viewmodel import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Content import org.linphone.core.tools.Log import org.linphone.ui.main.chat.model.FileModel import org.linphone.utils.Event import org.linphone.utils.LinphoneUtils +import kotlin.math.min class ConversationDocumentsListViewModel @UiThread constructor() : AbstractConversationViewModel() { companion object { private const val TAG = "[Conversation Documents List ViewModel]" + + private const val CONTENTS_PER_PAGE = 20 } val documentsList = MutableLiveData>() @@ -42,6 +47,8 @@ class ConversationDocumentsListViewModel MutableLiveData>() } + private var totalDocumentsCount: Int = -1 + @WorkerThread override fun afterNotifyingChatRoomFound(sameOne: Boolean) { loadDocumentsList() @@ -56,16 +63,48 @@ class ConversationDocumentsListViewModel @WorkerThread private fun loadDocumentsList() { operationInProgress.postValue(true) - - val list = arrayListOf() Log.i( "$TAG Loading document contents for conversation [${LinphoneUtils.getConversationId( chatRoom )}]" ) - val documents = chatRoom.documentContents - Log.i("$TAG [${documents.size}] documents have been fetched") - for (documentContent in documents) { + + totalDocumentsCount = chatRoom.documentContentsSize + Log.i("$TAG Document contents size is [$totalDocumentsCount]") + + val contentsToLoad = min(totalDocumentsCount, CONTENTS_PER_PAGE) + val contents = chatRoom.getDocumentContentsRange(0, contentsToLoad) + Log.i("$TAG [${contents.size}] documents have been fetched") + + documentsList.postValue(getFileModelsListFromContents(contents)) + operationInProgress.postValue(false) + } + + @UiThread + fun loadMoreData(totalItemsCount: Int) { + coreContext.postOnCoreThread { + Log.i("$TAG Loading more data, current total is $totalItemsCount, max size is $totalDocumentsCount") + + if (totalItemsCount < totalDocumentsCount) { + var upperBound: Int = totalItemsCount + CONTENTS_PER_PAGE + if (upperBound > totalDocumentsCount) { + upperBound = totalDocumentsCount + } + val contents = chatRoom.getDocumentContentsRange(totalItemsCount, upperBound) + Log.i("$TAG [${contents.size}] contents loaded, adding them to list") + + val list = arrayListOf() + list.addAll(documentsList.value.orEmpty()) + list.addAll(getFileModelsListFromContents(contents)) + documentsList.postValue(list) + } + } + } + + @WorkerThread + private fun getFileModelsListFromContents(contents: Array): ArrayList { + val list = arrayListOf() + for (documentContent in contents) { val isEncrypted = documentContent.isFileEncrypted val originalPath = documentContent.filePath.orEmpty() val path = if (isEncrypted) { @@ -94,14 +133,11 @@ class ConversationDocumentsListViewModel val model = FileModel(path, name, size, timestamp, isEncrypted, originalPath, ephemeral) { - openDocumentEvent.postValue(Event(it)) - } + openDocumentEvent.postValue(Event(it)) + } list.add(model) } } - - Log.i("$TAG [${documents.size}] documents have been processed") - documentsList.postValue(list) - operationInProgress.postValue(false) + return list } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt index bad0ea6724..f9b211f0ef 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt @@ -22,16 +22,21 @@ package org.linphone.ui.main.chat.viewmodel import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Content import org.linphone.core.tools.Log import org.linphone.ui.main.chat.model.FileModel import org.linphone.utils.Event import org.linphone.utils.LinphoneUtils +import kotlin.math.min class ConversationMediaListViewModel @UiThread constructor() : AbstractConversationViewModel() { companion object { private const val TAG = "[Conversation Media List ViewModel]" + + private const val CONTENTS_PER_PAGE = 50 } val mediaList = MutableLiveData>() @@ -42,6 +47,8 @@ class ConversationMediaListViewModel MutableLiveData>() } + private var totalMediaCount: Int = -1 + @WorkerThread override fun afterNotifyingChatRoomFound(sameOne: Boolean) { loadMediaList() @@ -56,16 +63,48 @@ class ConversationMediaListViewModel @WorkerThread private fun loadMediaList() { operationInProgress.postValue(true) - - val list = arrayListOf() Log.i( "$TAG Loading media contents for conversation [${LinphoneUtils.getConversationId( chatRoom )}]" ) - val media = chatRoom.mediaContents - Log.i("$TAG [${media.size}] media have been fetched") - for (mediaContent in media) { + + totalMediaCount = chatRoom.mediaContentsSize + Log.i("$TAG Media contents size is [$totalMediaCount]") + + val contentsToLoad = min(totalMediaCount, CONTENTS_PER_PAGE) + val contents = chatRoom.getMediaContentsRange(0, contentsToLoad) + Log.i("$TAG [${contents.size}] media have been fetched") + + mediaList.postValue(getFileModelsListFromContents(contents)) + operationInProgress.postValue(false) + } + + @UiThread + fun loadMoreData(totalItemsCount: Int) { + coreContext.postOnCoreThread { + Log.i("$TAG Loading more data, current total is $totalItemsCount, max size is $totalMediaCount") + + if (totalItemsCount < totalMediaCount) { + var upperBound: Int = totalItemsCount + CONTENTS_PER_PAGE + if (upperBound > totalMediaCount) { + upperBound = totalMediaCount + } + val contents = chatRoom.getMediaContentsRange(totalItemsCount, upperBound) + Log.i("$TAG [${contents.size}] contents loaded, adding them to list") + + val list = arrayListOf() + list.addAll(mediaList.value.orEmpty()) + list.addAll(getFileModelsListFromContents(contents)) + mediaList.postValue(list) + } + } + } + + @WorkerThread + private fun getFileModelsListFromContents(contents: Array): ArrayList { + val list = arrayListOf() + for (mediaContent in contents) { // Do not display voice recordings here, even if they are media file if (mediaContent.isVoiceRecording) continue @@ -85,14 +124,11 @@ class ConversationMediaListViewModel if (path.isNotEmpty() && name.isNotEmpty()) { val model = FileModel(path, name, size, timestamp, isEncrypted, originalPath, chatRoom.isEphemeralEnabled) { - openMediaEvent.postValue(Event(it)) - } + openMediaEvent.postValue(Event(it)) + } list.add(model) } } - - Log.i("$TAG [${media.size}] media have been processed") - mediaList.postValue(list) - operationInProgress.postValue(false) + return list } } From 8f3415f6fafcd85f8195040e5d3c0a437f69ce01 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 22 Sep 2025 14:58:44 +0200 Subject: [PATCH 298/593] Reduce limit of attempts to change audio device in Android framework + return correct value if it failed --- .../java/org/linphone/telecom/TelecomCallControlCallback.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt index d465f71e56..46b31ff90a 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt @@ -260,13 +260,14 @@ class TelecomCallControlCallback( continue } + var success = false scope.launch { Log.i("$TAG Requesting audio endpoint change to [${endpoint.name}] with type [${endpointTypeToString(endpoint.type)}]") endpointUpdateRequestFromLinphone = true latestLinphoneRequestedEndpoint = endpoint var result: CallControlResult = callControl.requestEndpointChange(endpoint) var attempts = 1 - while (result is CallControlResult.Error && attempts <= 10) { + while (result is CallControlResult.Error && attempts <= 2) { delay(100) Log.i( "$TAG Previous attempt failed [$result], requesting again audio endpoint change to [${endpoint.name}] with type [${endpointTypeToString(endpoint.type)}]" @@ -282,10 +283,11 @@ class TelecomCallControlCallback( "$TAG It took [$attempts] attempt(s) to change endpoint audio device..." ) currentEndpoint = endpoint.type + success = true } } - return true + return success } } From 1fdc2bcc58970ba2faabfa1ee828f98fd2b15911 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 22 Sep 2025 16:27:45 +0200 Subject: [PATCH 299/593] Fixed no suggestion flag not applied for filter text input in some screens (mostly participant pickers) --- app/src/main/res/layout/call_transfer_fragment.xml | 2 +- app/src/main/res/layout/chat_message_forward_fragment.xml | 2 +- app/src/main/res/layout/generic_add_participants_fragment.xml | 2 +- app/src/main/res/layout/start_call_fragment.xml | 2 +- app/src/main/res/layout/start_chat_fragment.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/layout/call_transfer_fragment.xml b/app/src/main/res/layout/call_transfer_fragment.xml index 58e780b427..9dba80158b 100644 --- a/app/src/main/res/layout/call_transfer_fragment.xml +++ b/app/src/main/res/layout/call_transfer_fragment.xml @@ -121,7 +121,7 @@ android:drawablePadding="10dp" android:drawableTint="?attr/color_main2_600" android:hint="@string/history_call_start_search_bar_filter_hint" - android:inputType="textPersonName|textNoSuggestions" + android:inputType="text|textPersonName|textNoSuggestions" android:paddingStart="15dp" android:paddingTop="10dp" android:paddingEnd="45dp" diff --git a/app/src/main/res/layout/chat_message_forward_fragment.xml b/app/src/main/res/layout/chat_message_forward_fragment.xml index e9c69ba0be..7941b6b5e6 100644 --- a/app/src/main/res/layout/chat_message_forward_fragment.xml +++ b/app/src/main/res/layout/chat_message_forward_fragment.xml @@ -70,7 +70,7 @@ android:drawablePadding="10dp" android:drawableTint="?attr/color_main2_600" android:hint="@string/new_conversation_search_bar_filter_hint" - android:inputType="textPersonName|textNoSuggestions" + android:inputType="text|textPersonName|textNoSuggestions" android:paddingStart="15dp" android:paddingTop="10dp" android:paddingEnd="15dp" diff --git a/app/src/main/res/layout/generic_add_participants_fragment.xml b/app/src/main/res/layout/generic_add_participants_fragment.xml index 828f3697c7..dc933c55fa 100644 --- a/app/src/main/res/layout/generic_add_participants_fragment.xml +++ b/app/src/main/res/layout/generic_add_participants_fragment.xml @@ -99,7 +99,7 @@ android:drawablePadding="10dp" android:drawableTint="?attr/color_main2_600" android:hint="@string/new_conversation_search_bar_filter_hint" - android:inputType="textPersonName|textNoSuggestions" + android:inputType="text|textPersonName|textNoSuggestions" android:paddingStart="15dp" android:paddingTop="10dp" android:paddingEnd="15dp" diff --git a/app/src/main/res/layout/start_call_fragment.xml b/app/src/main/res/layout/start_call_fragment.xml index 3c321a7b1a..8bbb4d0c5e 100644 --- a/app/src/main/res/layout/start_call_fragment.xml +++ b/app/src/main/res/layout/start_call_fragment.xml @@ -116,7 +116,7 @@ android:drawablePadding="10dp" android:drawableTint="?attr/color_main2_600" android:hint="@string/history_call_start_search_bar_filter_hint" - android:inputType="textPersonName|textNoSuggestions" + android:inputType="text|textPersonName|textNoSuggestions" android:paddingStart="15dp" android:paddingTop="10dp" android:paddingEnd="45dp" diff --git a/app/src/main/res/layout/start_chat_fragment.xml b/app/src/main/res/layout/start_chat_fragment.xml index 1f04687b35..86b77a41ef 100644 --- a/app/src/main/res/layout/start_chat_fragment.xml +++ b/app/src/main/res/layout/start_chat_fragment.xml @@ -112,7 +112,7 @@ android:drawablePadding="10dp" android:drawableTint="?attr/color_main2_600" android:hint="@string/new_conversation_search_bar_filter_hint" - android:inputType="textPersonName|textNoSuggestions" + android:inputType="text|textPersonName|textNoSuggestions" android:paddingStart="15dp" android:paddingTop="10dp" android:paddingEnd="15dp" From 61517461dd227e5104b6551dcac36d97f494cf50 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 6 May 2025 11:35:21 +0200 Subject: [PATCH 300/593] Use account recovery token FlexiAPI endpoint --- .../fragment/AccountRecoverFragment.kt | 134 +++++++++++++ .../ui/assistant/fragment/LandingFragment.kt | 7 +- .../ui/assistant/fragment/RegisterFragment.kt | 10 - .../viewmodel/AccountCreationViewModel.kt | 89 ++++++--- app/src/main/res/drawable/password.xml | 9 + .../assistant_recover_account_fragment.xml | 181 ++++++++++++++++++ .../res/navigation/assistant_nav_graph.xml | 19 +- app/src/main/res/values-fr/strings.xml | 8 +- app/src/main/res/values/strings.xml | 10 +- 9 files changed, 421 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/org/linphone/ui/assistant/fragment/AccountRecoverFragment.kt create mode 100644 app/src/main/res/drawable/password.xml create mode 100644 app/src/main/res/layout/assistant_recover_account_fragment.xml diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/AccountRecoverFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/AccountRecoverFragment.kt new file mode 100644 index 0000000000..93b1a0bbef --- /dev/null +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/AccountRecoverFragment.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2010-2025 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.ui.assistant.fragment + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.UiThread +import androidx.core.net.toUri +import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels +import org.linphone.R +import org.linphone.core.tools.Log +import org.linphone.ui.GenericFragment +import org.linphone.databinding.AssistantRecoverAccountFragmentBinding +import org.linphone.ui.assistant.viewmodel.AccountCreationViewModel +import kotlin.getValue + +@UiThread +class RecoverAccountFragment : GenericFragment() { + companion object { + private const val TAG = "[Recover Account Fragment]" + } + + private lateinit var binding: AssistantRecoverAccountFragmentBinding + + private val viewModel: AccountCreationViewModel by navGraphViewModels( + R.id.assistant_nav_graph + ) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AssistantRecoverAccountFragmentBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel + observeToastEvents(viewModel) + + viewModel.accountRecoveryTokenReceivedEvent.observe(viewLifecycleOwner) { + it.consume { token -> + Log.i("$TAG Account recovery token received [$token], opening browser") + recoverPhoneNumberAccount(token) + } + } + + binding.setBackClickListener { + goBack() + } + + binding.setRecoverEmailAccountClickListener { + recoverEmailAccount() + } + + binding.setRecoverPhoneNumberAccountClickListener { + viewModel.requestAccountRecoveryToken() + } + } + + private fun goBack() { + findNavController().popBackStack() + } + + private fun recoverEmailAccount() { + val rootUrl = getString(R.string.web_platform_forgotten_password_url) + val url = "$rootUrl/recovery/email" + try { + Log.i("$TAG Trying to open [$url] URL") + val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) + startActivity(browserIntent) + } catch (ise: IllegalStateException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" + ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" + ) + } catch (e: Exception) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" + ) + } + } + + private fun recoverPhoneNumberAccount(recoveryToken: String) { + val rootUrl = getString(R.string.web_platform_forgotten_password_url) + val url = "$rootUrl/recovery/phone/$recoveryToken" + try { + Log.i("$TAG Trying to open [$url] URL") + val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) + startActivity(browserIntent) + } catch (ise: IllegalStateException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" + ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" + ) + } catch (e: Exception) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" + ) + } + } +} diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt index 7ab1072451..9ec95a50ec 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt @@ -111,8 +111,11 @@ class LandingFragment : GenericFragment() { } binding.setForgottenPasswordClickListener { - val url = getString(R.string.web_platform_forgotten_password_url) - openUrlInBrowser(url) + if (findNavController().currentDestination?.id == R.id.landingFragment) { + val action = + LandingFragmentDirections.actionLandingFragmentToRecoverAccountFragment() + findNavController().navigate(action) + } } viewModel.showPassword.observe(viewLifecycleOwner) { diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt index 1afb07dea1..6790a77197 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt @@ -42,7 +42,6 @@ import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R import org.linphone.core.tools.Log import org.linphone.databinding.AssistantRegisterFragmentBinding -import org.linphone.ui.GenericActivity import org.linphone.ui.GenericFragment import org.linphone.ui.assistant.viewmodel.AccountCreationViewModel import org.linphone.utils.ConfirmationDialogModel @@ -164,15 +163,6 @@ class RegisterFragment : GenericFragment() { } } - viewModel.errorHappenedEvent.observe(viewLifecycleOwner) { - it.consume { error -> - (requireActivity() as GenericActivity).showRedToast( - error, - R.drawable.warning_circle - ) - } - } - val telephonyManager = requireContext().getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager val countryIso = telephonyManager.networkCountryIso coreContext.postOnCoreThread { diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt index 2681eda1c2..912e339670 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt @@ -47,7 +47,6 @@ import org.linphone.core.Dictionary import org.linphone.core.Factory import org.linphone.core.tools.Log import org.linphone.ui.GenericViewModel -import org.linphone.utils.AppUtils import org.linphone.utils.Event import org.linphone.utils.LinphoneUtils @@ -105,7 +104,7 @@ class AccountCreationViewModel val accountCreatedEvent = MutableLiveData>() - val errorHappenedEvent: MutableLiveData> by lazy { + val accountRecoveryTokenReceivedEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -113,7 +112,9 @@ class AccountCreationViewModel private var waitForPushJob: Job? = null private lateinit var accountManagerServices: AccountManagerServices + private var requestedTokenIsForAccountCreation: Boolean = true private var accountCreationToken: String? = null + private var accountRecoveryToken: String? = null private var accountCreatedAuthInfo: AuthInfo? = null private var accountCreated: Account? = null @@ -124,7 +125,7 @@ class AccountCreationViewModel request: AccountManagerServicesRequest, data: String? ) { - Log.i("$TAG Request [$request] was successful, data is [$data]") + Log.i("$TAG Request [${request.type}] was successful, data is [$data]") operationInProgress.postValue(false) when (request.type) { @@ -138,6 +139,10 @@ class AccountCreationViewModel ) } } + AccountManagerServicesRequest.Type.SendAccountCreationTokenByPush, + AccountManagerServicesRequest.Type.SendAccountRecoveryTokenByPush -> { + Log.i("$TAG Send token by push notification request has been accepted, it should be received soon") + } AccountManagerServicesRequest.Type.SendPhoneNumberLinkingCodeBySms -> { goToSmsCodeConfirmationViewEvent.postValue(Event(true)) } @@ -156,7 +161,7 @@ class AccountCreationViewModel parameterErrors: Dictionary? ) { Log.e( - "$TAG Request [$request] returned an error with status code [$statusCode] and message [$errorMessage]" + "$TAG Request [${request.type}] returned an error with status code [$statusCode] and message [$errorMessage]" ) operationInProgress.postValue(false) @@ -174,7 +179,8 @@ class AccountCreationViewModel } when (request.type) { - AccountManagerServicesRequest.Type.SendAccountCreationTokenByPush -> { + AccountManagerServicesRequest.Type.SendAccountCreationTokenByPush, + AccountManagerServicesRequest.Type.SendAccountRecoveryTokenByPush -> { Log.w("$TAG Cancelling job waiting for push notification") waitingForFlexiApiPushToken = false waitForPushJob?.cancel() @@ -220,11 +226,19 @@ class AccountCreationViewModel val token = customPayload.getString("token") if (token.isNotEmpty()) { - accountCreationToken = token - Log.i( - "$TAG Extracted token [$accountCreationToken] from push payload, creating account" - ) - createAccount() + if (requestedTokenIsForAccountCreation) { + accountCreationToken = token + Log.i( + "$TAG Extracted token [$accountCreationToken] from push payload, creating account" + ) + createAccount() + } else { + accountRecoveryToken = token + Log.i( + "$TAG Extracted token [$accountRecoveryToken] from push payload, opening browser" + ) + accountRecoveryTokenReceivedEvent.postValue(Event(token)) + } } else { Log.e("$TAG Push payload JSON object has an empty 'token'!") onFlexiApiTokenRequestError() @@ -317,9 +331,7 @@ class AccountCreationViewModel normalizedPhoneNumberEvent.postValue(Event(formattedPhoneNumber)) } else { Log.e("$TAG Account manager services hasn't been initialized!") - errorHappenedEvent.postValue( - Event(AppUtils.getString(R.string.assistant_account_register_unexpected_error)) - ) + showRedToast(R.string.assistant_account_register_unexpected_error, R.drawable.warning_circle) } } } @@ -330,8 +342,8 @@ class AccountCreationViewModel coreContext.postOnCoreThread { if (accountCreationToken.isNullOrEmpty()) { - Log.i("$TAG We don't have a creation token, let's request one") - requestFlexiApiToken() + Log.i("$TAG We don't have an account creation token yet, let's request one") + requestFlexiApiToken(requestAccountCreationToken = true) } else { val authInfo = accountCreatedAuthInfo if (authInfo != null) { @@ -345,6 +357,20 @@ class AccountCreationViewModel } } + @UiThread + fun requestAccountRecoveryToken() { + coreContext.postOnCoreThread { + val existingToken = accountRecoveryToken + if (existingToken.isNullOrEmpty()) { + Log.i("$TAG We don't have an account recovery token yet, let's request one") + requestFlexiApiToken(requestAccountCreationToken = false) + } else { + Log.i("$TAG We've already have a token [$existingToken], using it") + accountRecoveryTokenReceivedEvent.postValue(Event(existingToken)) + } + } + } + @UiThread fun toggleShowPassword() { showPassword.value = showPassword.value == false @@ -365,7 +391,7 @@ class AccountCreationViewModel val account = accountCreated if (::accountManagerServices.isInitialized && account != null) { val code = - "${smsCodeFirstDigit.value}${smsCodeSecondDigit.value}${smsCodeThirdDigit.value}${smsCodeLastDigit.value}" + "${smsCodeFirstDigit.value.orEmpty().trim()}${smsCodeSecondDigit.value.orEmpty().trim()}${smsCodeThirdDigit.value.orEmpty().trim()}${smsCodeLastDigit.value.orEmpty().trim()}" val identity = account.params.identityAddress if (identity != null) { Log.i( @@ -519,7 +545,8 @@ class AccountCreationViewModel } @WorkerThread - private fun requestFlexiApiToken() { + private fun requestFlexiApiToken(requestAccountCreationToken: Boolean) { + requestedTokenIsForAccountCreation = requestAccountCreationToken if (!coreContext.core.isPushNotificationAvailable) { Log.e( "$TAG Core says push notification aren't available, can't request a token from FlexiAPI" @@ -545,11 +572,21 @@ class AccountCreationViewModel } // Request an auth token, will be sent by push - val request = accountManagerServices.createSendAccountCreationTokenByPushRequest( - provider, - param, - prid - ) + val request = if (requestAccountCreationToken) { + Log.i("$TAG Requesting account creation token") + accountManagerServices.createSendAccountCreationTokenByPushRequest( + provider, + param, + prid + ) + } else { + Log.i("$TAG Requesting account recovery token") + accountManagerServices.createSendAccountRecoveryTokenByPushRequest( + provider, + param, + prid + ) + } request.addListener(accountManagerServicesListener) request.submit() @@ -580,12 +617,6 @@ class AccountCreationViewModel private fun onFlexiApiTokenRequestError() { Log.e("$TAG Flexi API token request by push error!") operationInProgress.postValue(false) - errorHappenedEvent.postValue( - Event( - AppUtils.getString( - R.string.assistant_account_register_push_notification_not_received_error - ) - ) - ) + showRedToast(R.string.assistant_account_register_push_notification_not_received_error, R.drawable.warning_circle) } } diff --git a/app/src/main/res/drawable/password.xml b/app/src/main/res/drawable/password.xml new file mode 100644 index 0000000000..ee72b364f3 --- /dev/null +++ b/app/src/main/res/drawable/password.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/assistant_recover_account_fragment.xml b/app/src/main/res/layout/assistant_recover_account_fragment.xml new file mode 100644 index 0000000000..33531c8cb6 --- /dev/null +++ b/app/src/main/res/layout/assistant_recover_account_fragment.xml @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/assistant_nav_graph.xml b/app/src/main/res/navigation/assistant_nav_graph.xml index 6b7a5b9591..17e894f0c6 100644 --- a/app/src/main/res/navigation/assistant_nav_graph.xml +++ b/app/src/main/res/navigation/assistant_nav_graph.xml @@ -134,6 +134,7 @@ app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" app:launchSingleTop="true" /> + + app:popUpToInclusive="true" /> + - + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 40130bbcc8..ca7190ba6d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -141,6 +141,12 @@ Notifications : Pour vous informer quand vous recevez un message ou un appel. Microphone : Pour permettre à vos correspondants de vous entendre. Caméra : Pour capturer votre vidéo lors des appels et des conférences. + Récupération de compte + Choisissez comment récupérer votre compte. + Vous avez créé votre compte avec : + Un email + Un numéro de téléphone + Les notifications push ne semblent pas être disponibles sur votre appareil. Celles-ci sont nécessaires à la récupération d’un compte sur l’application mobile avec un numéro de téléphone. Contacts @@ -860,7 +866,7 @@ Passer - Mot de passe oublié ? + Mot de passe oublié ou inconnu ? Passer diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f3ae00480d..127c587e55 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,7 +37,7 @@ https://linphone.org/en/privacy-policy https://linphone.org/en/terms-of-use https://subscribe.linphone.org/register/email - https://subscribe.linphone.org/ + https://subscribe.linphone.org https://weblate.linphone.org/ https://wiki.linphone.org/xwiki/wiki/public/view/Linphone/Third%20party%20components%20/#Hlinphone-android https://linphone.org/en/features/#security @@ -183,6 +183,12 @@ Post notifications: To be informed when you receive a message or a call. Record audio: So your correspondent can hear you and to record voice messages. Access camera: To capture video during video calls and conferences. + Account recovery + Choose how to recover your account. + You created your account using: + An email + A phone number + Push notifications do not seem to be available on your device, but they are mandatory for recovering a phone number account in the mobile app. Contacts @@ -903,7 +909,7 @@ Skip - Forgotten password? + Forgotten or unknown password? Skip From 6cb78c8c597aaeb3d7f4671e5fea92a1e69f9b2a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 26 Sep 2025 11:57:52 +0200 Subject: [PATCH 301/593] Bumped dependencies --- gradle/libs.versions.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5e177ebc93..dcfc7f701f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] agp = "8.13.0" -kotlin = "2.2.10" +kotlin = "2.2.20" gmsGoogleServices = "4.4.3" firebaseCrashlytics = "3.0.6" -firebaseBomVersion = "34.2.0" +firebaseBomVersion = "34.3.0" #noinspection NewerVersionAvailable ktlint = "12.3.0" @@ -18,10 +18,10 @@ media = "1.7.1" recyclerview = "1.4.0" swipeRefreshLayout = "1.1.0" slidingpanelayout = "1.2.0" -window = "1.4.0" +window = "1.5.0" gridlayout = "1.1.0" securityCryptoKtx = "1.1.0" -navigation = "2.9.4" +navigation = "2.9.5" emoji2 = "1.6.0" car = "1.7.0" flexbox = "3.0.0" From c556d14fb08a12f0dab22be2f73f8a18536b798b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 29 Sep 2025 13:59:50 +0200 Subject: [PATCH 302/593] Fixed ConcurrentModificationException that could happen during contact edition --- .../viewmodel/ContactNewOrEditViewModel.kt | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt index 4f7d618008..da7c88ee02 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt @@ -262,10 +262,14 @@ class ContactNewOrEditViewModel fun addSipAddress(address: String = "", requestFieldToBeAddedInUi: Boolean = false) { val newModel = NewOrEditNumberOrAddressModel(address, true, "", { if (address.isEmpty()) { - addSipAddress(requestFieldToBeAddedInUi = true) + coreContext.postOnCoreThread { + addSipAddress(requestFieldToBeAddedInUi = true) + } } }, { model -> - removeModel(model) + coreContext.postOnCoreThread { + removeModel(model) + } }) sipAddresses.add(newModel) @@ -282,10 +286,14 @@ class ContactNewOrEditViewModel ) { val newModel = NewOrEditNumberOrAddressModel(number, false, label, { if (number.isEmpty()) { - addPhoneNumber(requestFieldToBeAddedInUi = true) + coreContext.postOnCoreThread { + addPhoneNumber(requestFieldToBeAddedInUi = true) + } } }, { model -> - removeModel(model) + coreContext.postOnCoreThread { + removeModel(model) + } }) phoneNumbers.add(newModel) @@ -294,14 +302,14 @@ class ContactNewOrEditViewModel } } - @UiThread + @WorkerThread private fun removeModel(model: NewOrEditNumberOrAddressModel) { if (model.isSip) { sipAddresses.remove(model) } else { phoneNumbers.remove(model) } - removeNewNumberOrAddressFieldEvent.value = Event(model) + removeNewNumberOrAddressFieldEvent.postValue(Event(model)) } @UiThread From 0ca4eba63b06834c64692cb7ede9bafda7dd21ec Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 1 Oct 2025 10:01:48 +0200 Subject: [PATCH 303/593] Fixed contacts presence subscribe being only enabled for default domain account, added setting to disable presence --- .../java/org/linphone/core/CoreContext.kt | 17 ---------- .../settings/viewmodel/SettingsViewModel.kt | 12 +++++++ .../ui/main/viewmodel/MainViewModel.kt | 1 - app/src/main/res/layout/settings_contacts.xml | 33 +++++++++++++++++-- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 45 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 80486fc6b1..e89e6d02d5 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -607,7 +607,6 @@ class CoreContext @WorkerThread fun startCore() { Log.i("$TAG Starting Core") - updateFriendListsSubscriptionDependingOnDefaultAccount() val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager audioManager.registerAudioDeviceCallback(audioDeviceCallback, coreThread) @@ -1055,22 +1054,6 @@ class CoreContext keepAliveServiceStarted = false } - @WorkerThread - fun updateFriendListsSubscriptionDependingOnDefaultAccount() { - val account = core.defaultAccount - if (account != null) { - val enabled = account.params.domain == corePreferences.defaultDomain - if (enabled != core.isFriendListSubscriptionEnabled) { - core.isFriendListSubscriptionEnabled = enabled - Log.i( - "$TAG Friend list(s) subscription are now ${if (enabled) "enabled" else "disabled"}" - ) - } - } else { - Log.e("$TAG Default account is null, do not touch friend lists subscription") - } - } - @WorkerThread fun playDtmf(character: Char, duration: Int = 200, ignoreSystemPolicy: Boolean = false) { try { diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 4d3e84efc8..2a79235a0e 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -119,6 +119,8 @@ class SettingsViewModel val cardDavFriendsLists = MutableLiveData>() + val presenceSubscribe = MutableLiveData() + val addLdapServerEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -342,6 +344,7 @@ class SettingsViewModel sortContactsBy.postValue(if (corePreferences.sortContactsByFirstName) 0 else 1) hideEmptyContacts.postValue(corePreferences.hideContactsWithoutPhoneNumberOrSipAddress) + presenceSubscribe.postValue(core.isFriendListSubscriptionEnabled) defaultLayout.postValue(core.defaultConferenceLayout.toInt()) @@ -643,6 +646,15 @@ class SettingsViewModel } } + @UiThread + fun togglePresenceSubscribe() { + val newValue = presenceSubscribe.value == false + coreContext.postOnCoreThread { core -> + core.isFriendListSubscriptionEnabled = newValue + presenceSubscribe.postValue(newValue) + } + } + @UiThread fun toggleMeetingsExpand() { expandMeetings.value = expandMeetings.value == false diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt index d2c8ec691f..0d84839f42 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt @@ -282,7 +282,6 @@ class MainViewModel Log.i( "$TAG Default account changed, now is [${account.params.identityAddress?.asStringUriOnly()}]" ) - coreContext.updateFriendListsSubscriptionDependingOnDefaultAccount() removeAlert(DEFAULT_ACCOUNT_DISABLED) removeAlert(NON_DEFAULT_ACCOUNT_NOT_CONNECTED) diff --git a/app/src/main/res/layout/settings_contacts.xml b/app/src/main/res/layout/settings_contacts.xml index 2230b8f327..d381c4d738 100644 --- a/app/src/main/res/layout/settings_contacts.xml +++ b/app/src/main/res/layout/settings_contacts.xml @@ -63,7 +63,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ca7190ba6d..6f50c63c4f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -227,6 +227,7 @@ Contacts Trier les contacts par Masquer les contacts sans adresse SIP ni numéro de téléphone + Souscrire aux informations de présence Ajouter un serveur LDAP Editer le serveur LDAP Ajouter un carnet d\'adresse CardDAV diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 127c587e55..953fd9ac75 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -269,6 +269,7 @@ Contacts Sort contacts by Hide contacts without SIP address nor phone number + Subscribe to presence info Add LDAP server Edit LDAP server Add CardDAV address book From df09bcad769ea6b3012fe03275613fa2b35832a3 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 1 Oct 2025 11:41:40 +0200 Subject: [PATCH 304/593] Hide SIP address field from contact editor when hide SIP addresses flag is set, and fixed issue where dialog with only 1 item would be displayed --- .../org/linphone/contacts/ContactsManager.kt | 25 ++++++++++--------- .../ui/call/fragment/ActiveCallFragment.kt | 9 ------- .../ui/call/viewmodel/CurrentCallViewModel.kt | 12 ++------- .../fragment/StartConversationFragment.kt | 10 -------- .../viewmodel/StartConversationViewModel.kt | 20 +++------------ .../viewmodel/ContactNewOrEditViewModel.kt | 6 +++++ .../contacts/viewmodel/ContactViewModel.kt | 12 ++------- .../history/viewmodel/HistoryViewModel.kt | 12 ++------- .../java/org/linphone/utils/LinphoneUtils.kt | 2 +- .../contact_new_or_edit_fragment.xml | 2 ++ .../layout/contact_new_or_edit_fragment.xml | 2 ++ 11 files changed, 34 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index efae440f3c..46f360b9cd 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -789,6 +789,7 @@ fun Friend.getPerson(): Person { @WorkerThread fun Friend.getListOfSipAddresses(): ArrayList
{ val addressesList = arrayListOf
() + if (corePreferences.hideSipAddresses) return addressesList for (address in addresses) { if (addressesList.find { it.weakEqual(address) } == null) { @@ -803,19 +804,19 @@ fun Friend.getListOfSipAddresses(): ArrayList
{ fun Friend.getListOfSipAddressesAndPhoneNumbers(listener: ContactNumberOrAddressClickListener): ArrayList { val addressesAndNumbers = arrayListOf() - if (!corePreferences.hideSipAddresses) { - for (address in getListOfSipAddresses()) { - val data = ContactNumberOrAddressModel( - this, - address, - address.asStringUriOnly(), - true, // SIP addresses are always enabled - listener, - true - ) - addressesAndNumbers.add(data) - } + // Will return an empty list if corePreferences.hideSipAddresses == true + for (address in getListOfSipAddresses()) { + val data = ContactNumberOrAddressModel( + this, + address, + address.asStringUriOnly(), + true, // SIP addresses are always enabled + listener, + true + ) + addressesAndNumbers.add(data) } + if (corePreferences.hidePhoneNumbers) { return addressesAndNumbers } diff --git a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt index 0d1cac9fc1..ead2822ca9 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt @@ -361,15 +361,6 @@ class ActiveCallFragment : GenericCallFragment() { } } - callViewModel.chatRoomCreationErrorEvent.observe(viewLifecycleOwner) { - it.consume { error -> - (requireActivity() as GenericActivity).showRedToast( - getString(error), - R.drawable.warning_circle - ) - } - } - callViewModel.goToConversationEvent.observe(viewLifecycleOwner) { it.consume { conversationId -> if (findNavController().currentDestination?.id == R.id.activeCallFragment) { diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 169c1a66bc..7ad6865378 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -220,10 +220,6 @@ class CurrentCallViewModel MutableLiveData>() } - val chatRoomCreationErrorEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - // Conference val conferenceModel = ConferenceViewModel() @@ -429,9 +425,7 @@ class CurrentCallViewModel Log.e("$TAG Conversation [$id] creation has failed!") chatRoom.removeListener(this) operationInProgress.postValue(false) - chatRoomCreationErrorEvent.postValue( - Event(R.string.conversation_failed_to_create_toast) - ) + showRedToast(R.string.conversation_failed_to_create_toast, R.drawable.warning_circle) } } } @@ -1434,9 +1428,7 @@ class CurrentCallViewModel "$TAG Failed to create 1-1 conversation with [${remoteAddress.asStringUriOnly()}]!" ) operationInProgress.postValue(false) - chatRoomCreationErrorEvent.postValue( - Event(R.string.conversation_failed_to_create_toast) - ) + showRedToast(R.string.conversation_failed_to_create_toast, R.drawable.warning_circle) } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/StartConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/StartConversationFragment.kt index 807ea9fe3c..84faf42476 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/StartConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/StartConversationFragment.kt @@ -99,16 +99,6 @@ class StartConversationFragment : GenericAddressPickerFragment() { } } - viewModel.chatRoomCreationErrorEvent.observe(viewLifecycleOwner) { - it.consume { error -> - Log.i("$TAG Conversation creation error, showing red toast") - (requireActivity() as GenericActivity).showRedToast( - getString(error), - R.drawable.warning_circle - ) - } - } - viewModel.defaultAccountChangedEvent.observe(viewLifecycleOwner) { it.consume { viewModel.updateGroupChatButtonVisibility() diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/StartConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/StartConversationViewModel.kt index 98e0a6920c..2a428d532c 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/StartConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/StartConversationViewModel.kt @@ -52,10 +52,6 @@ class StartConversationViewModel val operationInProgress = MutableLiveData() - val chatRoomCreationErrorEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - val chatRoomCreatedEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -78,9 +74,7 @@ class StartConversationViewModel Log.e("$TAG Conversation [$id] creation has failed!") chatRoom.removeListener(this) operationInProgress.postValue(false) - chatRoomCreationErrorEvent.postValue( - Event(R.string.conversation_failed_to_create_toast) - ) + showRedToast(R.string.conversation_failed_to_create_toast, R.drawable.warning_circle) } } } @@ -159,9 +153,7 @@ class StartConversationViewModel } else { Log.e("$TAG Failed to create group conversation [$groupChatRoomSubject]!") operationInProgress.postValue(false) - chatRoomCreationErrorEvent.postValue( - Event(R.string.conversation_failed_to_create_toast) - ) + showRedToast(R.string.conversation_failed_to_create_toast, R.drawable.warning_circle) } } } @@ -212,9 +204,7 @@ class StartConversationViewModel "$TAG Account is in secure mode, can't chat with SIP address of different domain [${remote.asStringUriOnly()}]" ) operationInProgress.postValue(false) - chatRoomCreationErrorEvent.postValue( - Event(R.string.conversation_invalid_participant_due_to_security_mode_toast) - ) + showRedToast(R.string.conversation_invalid_participant_due_to_security_mode_toast, R.drawable.warning_circle) return } @@ -247,9 +237,7 @@ class StartConversationViewModel } else { Log.e("$TAG Failed to create 1-1 conversation with [${remote.asStringUriOnly()}]!") operationInProgress.postValue(false) - chatRoomCreationErrorEvent.postValue( - Event(R.string.conversation_failed_to_create_toast) - ) + showRedToast(R.string.conversation_failed_to_create_toast, R.drawable.warning_circle) } } else { Log.w( diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt index da7c88ee02..02bdf318ac 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt @@ -66,6 +66,8 @@ class ContactNewOrEditViewModel val sipAddresses = ArrayList() + val hideSipAddresses = MutableLiveData() + val phoneNumbers = ArrayList() val company = MutableLiveData() @@ -82,6 +84,10 @@ class ContactNewOrEditViewModel val removeNewNumberOrAddressFieldEvent = MutableLiveData>() + init { + hideSipAddresses.postValue(corePreferences.hideSipAddresses) + } + @UiThread fun findFriendByRefKey(refKey: String?) { reset() diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt index e90988a892..ac2ea42543 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt @@ -99,10 +99,6 @@ class ContactViewModel val operationInProgress = MutableLiveData() - val chatRoomCreationErrorEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - val showLongPressMenuForNumberOrAddressEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -214,9 +210,7 @@ class ContactViewModel Log.e("$TAG Conversation [$id] creation has failed!") chatRoom.removeListener(this) operationInProgress.postValue(false) - chatRoomCreationErrorEvent.postValue( - Event(R.string.conversation_failed_to_create_toast) - ) + showRedToast(R.string.conversation_failed_to_create_toast, R.drawable.warning_circle) } } } @@ -579,9 +573,7 @@ class ContactViewModel "$TAG Failed to create 1-1 conversation with [${remote.asStringUriOnly()}]!" ) operationInProgress.postValue(false) - chatRoomCreationErrorEvent.postValue( - Event(R.string.conversation_failed_to_create_toast) - ) + showRedToast(R.string.conversation_failed_to_create_toast, R.drawable.warning_circle) } } } diff --git a/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt b/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt index 5cc06ba3d2..2da4057217 100644 --- a/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt @@ -67,10 +67,6 @@ class HistoryViewModel val callLogFoundEvent = MutableLiveData>() - val chatRoomCreationErrorEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - val goToMeetingConversationEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -125,9 +121,7 @@ class HistoryViewModel Log.e("$TAG Conversation [$id] creation has failed!") chatRoom.removeListener(this) operationInProgress.postValue(false) - chatRoomCreationErrorEvent.postValue( - Event(R.string.conversation_failed_to_create_toast) - ) + showRedToast(R.string.conversation_failed_to_create_toast, R.drawable.warning_circle) } } } @@ -307,9 +301,7 @@ class HistoryViewModel "$TAG Failed to create 1-1 conversation with [${remote.asStringUriOnly()}]!" ) operationInProgress.postValue(false) - chatRoomCreationErrorEvent.postValue( - Event(R.string.conversation_failed_to_create_toast) - ) + showRedToast(R.string.conversation_failed_to_create_toast, R.drawable.warning_circle) } } } diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 63907f3ddc..511df23d78 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -142,7 +142,7 @@ class LinphoneUtils { } val defaultDomain = corePreferences.defaultDomain - val currentDomain = friend.core.defaultAccount?.params?.identityAddress?.domain + val currentDomain = getDefaultAccount()?.params?.identityAddress?.domain if (defaultDomain != currentDomain) return null var defaultDomainAddressesCount = 0 diff --git a/app/src/main/res/layout-land/contact_new_or_edit_fragment.xml b/app/src/main/res/layout-land/contact_new_or_edit_fragment.xml index 9fe2cc4587..d86f806823 100644 --- a/app/src/main/res/layout-land/contact_new_or_edit_fragment.xml +++ b/app/src/main/res/layout-land/contact_new_or_edit_fragment.xml @@ -316,6 +316,7 @@ android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:text="@string/sip_address" + android:visibility="@{viewModel.hideSipAddresses ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="@id/sip_addresses"/> @@ -326,6 +327,7 @@ android:layout_marginStart="16dp" android:layout_marginEnd="16dp" android:orientation="vertical" + android:visibility="@{viewModel.hideSipAddresses ? View.GONE : View.VISIBLE}" app:layout_constraintWidth_max="@dimen/text_input_max_width" app:layout_constraintTop_toBottomOf="@id/sip_addresses_label" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/contact_new_or_edit_fragment.xml b/app/src/main/res/layout/contact_new_or_edit_fragment.xml index b9526e0e10..d0580d955c 100644 --- a/app/src/main/res/layout/contact_new_or_edit_fragment.xml +++ b/app/src/main/res/layout/contact_new_or_edit_fragment.xml @@ -224,6 +224,7 @@ android:layout_marginTop="30dp" android:layout_marginEnd="16dp" android:text="@string/sip_address" + android:visibility="@{viewModel.hideSipAddresses ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toBottomOf="@id/last_name" app:layout_constraintStart_toStartOf="@id/sip_addresses"/> @@ -234,6 +235,7 @@ android:layout_marginStart="16dp" android:layout_marginEnd="16dp" android:orientation="vertical" + android:visibility="@{viewModel.hideSipAddresses ? View.GONE : View.VISIBLE}" app:layout_constraintWidth_max="@dimen/text_input_max_width" app:layout_constraintTop_toBottomOf="@id/sip_addresses_label" app:layout_constraintStart_toStartOf="parent" From f8556aa46b95aea0c8006e3174e2154ee01229bf Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 1 Oct 2025 15:12:14 +0200 Subject: [PATCH 305/593] Hide suggestions SIP address domain if it matches default account SIP identity one + fixed suggestion avatar for phone numbers --- .../ConversationContactOrSuggestionModel.kt | 9 ++++++++- .../main/viewmodel/AddressSelectionViewModel.kt | 16 ++++++++++++++-- ...eric_address_picker_suggestion_list_cell.xml | 17 ++++++++--------- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/model/ConversationContactOrSuggestionModel.kt b/app/src/main/java/org/linphone/ui/main/model/ConversationContactOrSuggestionModel.kt index c477e7400a..865de117c3 100644 --- a/app/src/main/java/org/linphone/ui/main/model/ConversationContactOrSuggestionModel.kt +++ b/app/src/main/java/org/linphone/ui/main/model/ConversationContactOrSuggestionModel.kt @@ -36,6 +36,7 @@ class ConversationContactOrSuggestionModel val conversationId: String = "", conversationSubject: String? = null, val friend: Friend? = null, + val defaultAccountDomain: String? = null, private val onClicked: ((Address) -> Unit)? = null ) { val id = friend?.refKey ?: address.asStringUriOnly().hashCode() @@ -50,7 +51,13 @@ class ConversationContactOrSuggestionModel } val sipUri = if (!corePreferences.hideSipAddresses) { - address.asStringUriOnly() + // Hide SIP address and only show username for suggestions + // on the same domain as the currently selected account + if (!defaultAccountDomain.isNullOrEmpty() && defaultAccountDomain == address.domain) { + address.username + } else { + address.asStringUriOnly() + } } else { address.username } diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt index 590010450c..aaad253228 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt @@ -316,6 +316,7 @@ abstract class AddressSelectionViewModel arrayListOf() } + val defaultAccountDomain = LinphoneUtils.getDefaultAccount()?.params?.domain val favoritesList = arrayListOf() val domain = corePreferences.contactsFilter // Make a quick synchronous search for favorites (in case of total results exceed magic search limit to prevent missing ones) @@ -370,6 +371,8 @@ abstract class AddressSelectionViewModel val model = ConversationContactOrSuggestionModel(address) { coreContext.startAudioCall(address) } + val avatarModel = getContactAvatarModelForAddress(address) + model.avatarModel.postValue(avatarModel) suggestionsList.add(model) continue } @@ -380,10 +383,11 @@ abstract class AddressSelectionViewModel continue } - val model = ConversationContactOrSuggestionModel(address) { + val model = ConversationContactOrSuggestionModel(address, defaultAccountDomain = defaultAccountDomain) { coreContext.startAudioCall(address) } - + val avatarModel = getContactAvatarModelForAddress(address) + model.avatarModel.postValue(avatarModel) suggestionsList.add(model) } } @@ -573,4 +577,12 @@ abstract class AddressSelectionViewModel clearFilter() } } + + @WorkerThread + private fun getContactAvatarModelForAddress(address: Address): ContactAvatarModel { + val fakeFriend = coreContext.core.createFriend() + fakeFriend.name = LinphoneUtils.getDisplayName(address) + fakeFriend.address = address + return ContactAvatarModel(fakeFriend) + } } diff --git a/app/src/main/res/layout/generic_address_picker_suggestion_list_cell.xml b/app/src/main/res/layout/generic_address_picker_suggestion_list_cell.xml index a0fecb9052..134c12a918 100644 --- a/app/src/main/res/layout/generic_address_picker_suggestion_list_cell.xml +++ b/app/src/main/res/layout/generic_address_picker_suggestion_list_cell.xml @@ -1,7 +1,7 @@ - + @@ -23,15 +23,14 @@ android:paddingBottom="5dp" android:background="@drawable/primary_cell_background"> - From 6dc4790597ccecc4c08d40ae1142663cb084964c Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 2 Oct 2025 10:49:38 +0200 Subject: [PATCH 306/593] Simplified code using newly added API in SDK --- .../ui/call/conference/viewmodel/ConferenceViewModel.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt index 6f4a1c3824..e6b1ac40d2 100644 --- a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt @@ -125,10 +125,13 @@ class ConferenceViewModel conference: Conference, device: ParticipantDevice ) { - if (conference.isMe(device.address)) { + if (device.isMe) { + Log.i("$TAG Our device media capability changed") val direction = device.getStreamCapability(StreamType.Video) val sendingVideo = direction == MediaDirection.SendRecv || direction == MediaDirection.SendOnly localVideoStreamToggled(sendingVideo) + } else { + Log.i("$TAG Participant [${device.address.asStringUriOnly()}] device media capability changed") } } From 416cc6ea7f0cb009fec3c45b5a2871072a6dcdde Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 2 Oct 2025 15:17:05 +0200 Subject: [PATCH 307/593] Only display missing permissions in assistant PermissionsFragment --- .../assistant/fragment/PermissionsFragment.kt | 13 ++++- .../viewmodel/PermissionsViewModel.kt | 52 +++++++++++++++++++ .../layout/assistant_permissions_fragment.xml | 11 ++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/org/linphone/ui/assistant/viewmodel/PermissionsViewModel.kt diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/PermissionsFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/PermissionsFragment.kt index 562ae89364..3af77bfd03 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/PermissionsFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/PermissionsFragment.kt @@ -29,6 +29,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.UiThread import androidx.core.content.ContextCompat import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R import org.linphone.compatibility.Compatibility @@ -36,6 +37,8 @@ import org.linphone.core.tools.Log import org.linphone.databinding.AssistantPermissionsFragmentBinding import org.linphone.ui.GenericFragment import org.linphone.ui.assistant.AssistantActivity +import org.linphone.ui.assistant.viewmodel.PermissionsViewModel +import kotlin.getValue @UiThread class PermissionsFragment : GenericFragment() { @@ -45,6 +48,10 @@ class PermissionsFragment : GenericFragment() { private lateinit var binding: AssistantPermissionsFragmentBinding + private val viewModel: PermissionsViewModel by navGraphViewModels( + R.id.assistant_nav_graph + ) + private var leaving = false private val requestPermissionLauncher = registerForActivityResult( @@ -93,6 +100,7 @@ class PermissionsFragment : GenericFragment() { super.onViewCreated(view, savedInstanceState) binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel binding.setBackClickListener { findNavController().popBackStack() @@ -180,10 +188,13 @@ class PermissionsFragment : GenericFragment() { private fun areAllPermissionsGranted(): Boolean { for (permission in Compatibility.getAllRequiredPermissionsArray()) { - if (ContextCompat.checkSelfPermission(requireContext(), permission) != PackageManager.PERMISSION_GRANTED) { + val granted = ContextCompat.checkSelfPermission(requireContext(), permission) == PackageManager.PERMISSION_GRANTED + viewModel.setPermissionGranted(permission, granted) + if (!granted) { Log.w("$TAG Permission [$permission] hasn't been granted yet!") return false } + } return Compatibility.hasFullScreenIntentPermission(requireContext()) } diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/PermissionsViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/PermissionsViewModel.kt new file mode 100644 index 0000000000..9030a56893 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/PermissionsViewModel.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2010-2025 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.ui.assistant.viewmodel + +import android.Manifest +import androidx.annotation.UiThread +import androidx.lifecycle.MutableLiveData +import org.linphone.core.tools.Log +import org.linphone.ui.GenericViewModel + +class PermissionsViewModel +@UiThread +constructor() : GenericViewModel() { + companion object { + private const val TAG = "[Permissions ViewModel]" + } + + val cameraPermissionGranted = MutableLiveData() + + val recordAudioPermissionGranted = MutableLiveData() + + val readContactsPermissionGranted = MutableLiveData() + + val postNotificationsPermissionGranted = MutableLiveData() + + fun setPermissionGranted(permission: String, granted: Boolean) { + Log.i("$TAG Permission [$permission] is ${if (granted) "granted" else "not granted yet/denied"}") + when (permission) { + Manifest.permission.READ_CONTACTS -> readContactsPermissionGranted.postValue(granted) + Manifest.permission.RECORD_AUDIO -> recordAudioPermissionGranted.postValue(granted) + Manifest.permission.CAMERA -> cameraPermissionGranted.postValue(granted) + Manifest.permission.POST_NOTIFICATIONS -> postNotificationsPermissionGranted.postValue(granted) + } + } +} diff --git a/app/src/main/res/layout/assistant_permissions_fragment.xml b/app/src/main/res/layout/assistant_permissions_fragment.xml index f487daf3b7..2b7e608a97 100644 --- a/app/src/main/res/layout/assistant_permissions_fragment.xml +++ b/app/src/main/res/layout/assistant_permissions_fragment.xml @@ -26,6 +26,9 @@ + @@ -100,6 +104,7 @@ android:text="@string/assistant_permissions_post_notifications_title" android:maxLines="2" android:ellipsize="end" + android:visibility="@{viewModel.postNotificationsPermissionGranted ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toTopOf="@id/post_notifications_icon" app:layout_constraintStart_toEndOf="@id/post_notifications_icon" app:layout_constraintBottom_toBottomOf="@id/post_notifications_icon" @@ -115,6 +120,7 @@ android:padding="12dp" android:src="@drawable/address_book" android:contentDescription="@null" + android:visibility="@{viewModel.readContactsPermissionGranted ? View.GONE : View.VISIBLE}" app:tint="?attr/color_main2_500" app:layout_constraintTop_toBottomOf="@id/post_notifications_icon" app:layout_constraintStart_toStartOf="parent"/> @@ -129,6 +135,7 @@ android:text="@string/assistant_permissions_read_contacts_title" android:maxLines="2" android:ellipsize="end" + android:visibility="@{viewModel.readContactsPermissionGranted ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toTopOf="@id/read_contacts_icon" app:layout_constraintStart_toEndOf="@id/read_contacts_icon" app:layout_constraintBottom_toBottomOf="@id/read_contacts_icon" @@ -144,6 +151,7 @@ android:padding="12dp" android:src="@drawable/microphone" android:contentDescription="@null" + android:visibility="@{viewModel.recordAudioPermissionGranted ? View.GONE : View.VISIBLE}" app:tint="?attr/color_main2_500" app:layout_constraintTop_toBottomOf="@id/read_contacts_icon" app:layout_constraintStart_toStartOf="parent"/> @@ -158,6 +166,7 @@ android:text="@string/assistant_permissions_record_audio_title" android:maxLines="2" android:ellipsize="end" + android:visibility="@{viewModel.recordAudioPermissionGranted ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toTopOf="@id/record_audio_icon" app:layout_constraintStart_toEndOf="@id/record_audio_icon" app:layout_constraintBottom_toBottomOf="@id/record_audio_icon" @@ -173,6 +182,7 @@ android:padding="12dp" android:src="@drawable/video_camera" android:contentDescription="@null" + android:visibility="@{viewModel.cameraPermissionGranted ? View.GONE : View.VISIBLE}" app:tint="?attr/color_main2_500" app:layout_constraintTop_toBottomOf="@id/record_audio_icon" app:layout_constraintStart_toStartOf="parent"/> @@ -187,6 +197,7 @@ android:text="@string/assistant_permissions_access_camera_title" android:maxLines="2" android:ellipsize="end" + android:visibility="@{viewModel.cameraPermissionGranted ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toTopOf="@id/access_camera_icon" app:layout_constraintStart_toEndOf="@id/access_camera_icon" app:layout_constraintBottom_toBottomOf="@id/access_camera_icon" From 2ea38abdfeb5862d3067b7718723f4736e508018 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 2 Oct 2025 15:51:27 +0200 Subject: [PATCH 308/593] Reworked proxy/outbound proxy settings in account advanced settings --- .../ThirdPartySipAccountLoginFragment.kt | 10 ++ .../ThirdPartySipAccountLoginViewModel.kt | 40 +++++-- .../fragment/AccountSettingsFragment.kt | 44 ++------ .../viewmodel/AccountSettingsViewModel.kt | 21 +++- .../java/org/linphone/utils/DialogUtils.kt | 18 ++++ ...third_party_sip_account_login_fragment.xml | 55 +++++++++- .../res/layout/account_advanced_settings.xml | 101 +++++++----------- .../res/layout/account_settings_fragment.xml | 4 + ...third_party_sip_account_login_fragment.xml | 55 +++++++++- ...log_manage_account_outbound_proxy_help.xml | 87 +++++++++++++++ app/src/main/res/values-fr/strings.xml | 6 +- app/src/main/res/values/strings.xml | 6 +- 12 files changed, 325 insertions(+), 122 deletions(-) create mode 100644 app/src/main/res/layout/dialog_manage_account_outbound_proxy_help.xml diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountLoginFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountLoginFragment.kt index 72cb21e996..ba33e9fd63 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountLoginFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountLoginFragment.kt @@ -41,6 +41,7 @@ import org.linphone.ui.GenericActivity import org.linphone.ui.GenericFragment import org.linphone.ui.assistant.viewmodel.ThirdPartySipAccountLoginViewModel import org.linphone.ui.main.sso.fragment.SingleSignOnFragmentDirections +import org.linphone.utils.DialogUtils import org.linphone.utils.PhoneNumberUtils @UiThread @@ -98,6 +99,10 @@ class ThirdPartySipAccountLoginFragment : GenericFragment() { goBack() } + binding.setOutboundProxyTooltipClickListener { + showOutboundProxyInfoDialog() + } + viewModel.showPassword.observe(viewLifecycleOwner) { lifecycleScope.launch { delay(50) @@ -159,4 +164,9 @@ class ThirdPartySipAccountLoginFragment : GenericFragment() { private fun goBack() { findNavController().popBackStack() } + + private fun showOutboundProxyInfoDialog() { + val dialog = DialogUtils.getAccountOutboundProxyHelpDialog(requireActivity()) + dialog.show() + } } diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt index 1b7c62adf3..1e0ffb9dbc 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt @@ -67,6 +67,8 @@ class ThirdPartySipAccountLoginViewModel val expandAdvancedSettings = MutableLiveData() + val proxy = MutableLiveData() + val outboundProxy = MutableLiveData() val loginEnabled = MediatorLiveData() @@ -236,25 +238,45 @@ class ThirdPartySipAccountLoginViewModel } accountParams.identityAddress = identityAddress - val outboundProxyValue = outboundProxy.value.orEmpty().trim() - val serverAddress = if (outboundProxyValue.isNotEmpty()) { - val server = if (outboundProxyValue.startsWith("sip:")) { - outboundProxyValue + val proxyServerValue = proxy.value.orEmpty().trim() + val proxyServerAddress = if (proxyServerValue.isNotEmpty()) { + val server = if (proxyServerValue.startsWith("sip:")) { + proxyServerValue } else { - "sip:$outboundProxyValue" + "sip:$proxyServerValue" } Factory.instance().createAddress(server) } else { domainAddress ?: Factory.instance().createAddress("sip:$domainWithoutSip") } - - serverAddress?.transport = when (transport.value.orEmpty().trim()) { + proxyServerAddress?.transport = when (transport.value.orEmpty().trim()) { TransportType.Tcp.name.uppercase(Locale.getDefault()) -> TransportType.Tcp TransportType.Tls.name.uppercase(Locale.getDefault()) -> TransportType.Tls else -> TransportType.Udp } - Log.i("$TAG Created proxy server SIP address [${serverAddress?.asStringUriOnly()}]") - accountParams.serverAddress = serverAddress + Log.i("$TAG Created proxy server SIP address [${proxyServerAddress?.asStringUriOnly()}]") + accountParams.serverAddress = proxyServerAddress + + val outboundProxyValue = outboundProxy.value.orEmpty().trim() + val outboundProxyAddress = if (outboundProxyValue.isNotEmpty()) { + val server = if (outboundProxyValue.startsWith("sip:")) { + outboundProxyValue + } else { + "sip:$outboundProxyValue" + } + Factory.instance().createAddress(server) + } else { + null + } + if (outboundProxyAddress != null) { + outboundProxyAddress.transport = when (transport.value.orEmpty().trim()) { + TransportType.Tcp.name.uppercase(Locale.getDefault()) -> TransportType.Tcp + TransportType.Tls.name.uppercase(Locale.getDefault()) -> TransportType.Tls + else -> TransportType.Udp + } + Log.i("$TAG Created outbound proxy server SIP address [${outboundProxyAddress?.asStringUriOnly()}]") + accountParams.setRoutesAddresses(arrayOf(outboundProxyAddress)) + } val prefix = internationalPrefix.value.orEmpty().trim() val isoCountryCode = internationalPrefixIsoCountryCode.value.orEmpty() diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/AccountSettingsFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/AccountSettingsFragment.kt index 9279438f03..ceec8bbb97 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/fragment/AccountSettingsFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/AccountSettingsFragment.kt @@ -23,16 +23,12 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter import androidx.annotation.UiThread import androidx.core.view.doOnPreDraw import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import java.util.Locale import org.linphone.R -import org.linphone.core.TransportType import org.linphone.core.tools.Log import org.linphone.databinding.AccountSettingsFragmentBinding import org.linphone.ui.GenericActivity @@ -54,22 +50,6 @@ class AccountSettingsFragment : GenericMainFragment() { private lateinit var viewModel: AccountSettingsViewModel - private val transportDropdownListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - val transport = viewModel.availableTransports[position] - val transportType = when (transport) { - TransportType.Tcp.name.uppercase(Locale.getDefault()) -> TransportType.Tcp - TransportType.Tls.name.uppercase(Locale.getDefault()) -> TransportType.Tls - else -> TransportType.Udp - } - Log.i("$TAG Selected transport updated [$transport] -> [${transportType.name}]") - viewModel.selectedTransport.value = transportType - } - - override fun onNothingSelected(parent: AdapterView<*>?) { - } - } - override fun goBack(): Boolean { try { return findNavController().popBackStack() @@ -110,13 +90,15 @@ class AccountSettingsFragment : GenericMainFragment() { showUpdatePasswordDialog() } + binding.setOutboundProxyTooltipClickListener { + showOutboundProxyInfoDialog() + } + viewModel.accountFoundEvent.observe(viewLifecycleOwner) { it.consume { found -> if (found) { (view.parent as? ViewGroup)?.doOnPreDraw { startPostponedEnterTransition() - - setupTransportDropdown() } } else { Log.e( @@ -159,20 +141,8 @@ class AccountSettingsFragment : GenericMainFragment() { dialog.show() } - private fun setupTransportDropdown() { - val adapter = ArrayAdapter( - requireContext(), - R.layout.drop_down_item, - viewModel.availableTransports - ) - adapter.setDropDownViewResource(R.layout.generic_dropdown_cell) - val currentTransport = viewModel.selectedTransport.value?.name?.uppercase( - Locale.getDefault() - ) - binding.accountAdvancedSettings.transportSpinner.adapter = adapter - binding.accountAdvancedSettings.transportSpinner.setSelection( - viewModel.availableTransports.indexOf(currentTransport) - ) - binding.accountAdvancedSettings.transportSpinner.onItemSelectedListener = transportDropdownListener + private fun showOutboundProxyInfoDialog() { + val dialog = DialogUtils.getAccountOutboundProxyHelpDialog(requireActivity()) + dialog.show() } } diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt index 57590fbd90..1aaafd2114 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt @@ -62,7 +62,7 @@ class AccountSettingsViewModel val sipProxyServer = MutableLiveData() - val outboundProxyEnabled = MutableLiveData() + val outboundProxyServer = MutableLiveData() val stunServer = MutableLiveData() @@ -155,7 +155,7 @@ class AccountSettingsViewModel selectedTransport.postValue(transportType) sipProxyServer.postValue(params.serverAddress?.asStringUriOnly()) - outboundProxyEnabled.postValue(params.isOutboundProxyEnabled) + outboundProxyServer.postValue(params.routesAddresses.first().asStringUriOnly()) natPolicy = params.natPolicy ?: core.createNatPolicy() stunServer.postValue(natPolicy.stunServer) @@ -222,13 +222,28 @@ class AccountSettingsViewModel val server = sipProxyServer.value.orEmpty() if (server.isNotEmpty()) { + Log.i("$TAG Proxy server set to [$server]") val serverAddress = core.interpretUrl(server, false) if (serverAddress != null) { serverAddress.transport = selectedTransport.value newParams.serverAddress = serverAddress + } else { + Log.e("$TAG Failed to parse proxy server!") } } - newParams.isOutboundProxyEnabled = outboundProxyEnabled.value == true + val outboundProxy = outboundProxyServer.value.orEmpty() + if (outboundProxy.isNotEmpty()) { + Log.i("$TAG Outbound proxy server set to [$outboundProxy]") + val outboundProxyAddress = core.interpretUrl(outboundProxy, false) + if (outboundProxyAddress != null) { + outboundProxyAddress.transport = selectedTransport.value + newParams.setRoutesAddresses(arrayOf(outboundProxyAddress)) + } else { + Log.e("$TAG Failed to parse outbound proxy server!") + } + } else { + newParams.setRoutesAddresses(null) + } if (::natPolicy.isInitialized) { Log.i("$TAG Also applying changes to NAT policy") diff --git a/app/src/main/java/org/linphone/utils/DialogUtils.kt b/app/src/main/java/org/linphone/utils/DialogUtils.kt index a1f003e779..b5860177f2 100644 --- a/app/src/main/java/org/linphone/utils/DialogUtils.kt +++ b/app/src/main/java/org/linphone/utils/DialogUtils.kt @@ -68,6 +68,7 @@ import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel import org.linphone.ui.main.model.GroupSetOrEditSubjectDialogModel import androidx.core.graphics.drawable.toDrawable import org.linphone.databinding.DialogDeleteChatMessageBinding +import org.linphone.databinding.DialogManageAccountOutboundProxyHelpBinding import org.linphone.ui.main.chat.model.MessageDeleteDialogModel class DialogUtils { @@ -122,6 +123,23 @@ class DialogUtils { return dialog } + @UiThread + fun getAccountOutboundProxyHelpDialog(context: Context): Dialog { + val binding: DialogManageAccountOutboundProxyHelpBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.dialog_manage_account_outbound_proxy_help, + null, + false + ) + val dialog = getDialog(context, binding) + + binding.setDismissClickListener { + dialog.dismiss() + } + + return dialog + } + @UiThread fun getConfirmAccountRemovalDialog( context: Context, diff --git a/app/src/main/res/layout-sw600dp/assistant_third_party_sip_account_login_fragment.xml b/app/src/main/res/layout-sw600dp/assistant_third_party_sip_account_login_fragment.xml index 705e52297c..f8b472c210 100644 --- a/app/src/main/res/layout-sw600dp/assistant_third_party_sip_account_login_fragment.xml +++ b/app/src/main/res/layout-sw600dp/assistant_third_party_sip_account_login_fragment.xml @@ -9,6 +9,9 @@ + @@ -312,7 +315,7 @@ android:id="@+id/advanced_settings_group" android:layout_width="wrap_content" android:layout_height="wrap_content" - app:constraint_referenced_ids="authentication_id_label, authentication_id, outbound_proxy_label, outbound_proxy" + app:constraint_referenced_ids="authentication_id_label, authentication_id, proxy, proxy_label, outbound_proxy_label, outbound_proxy, outbound_proxy_tooltip" android:visibility="@{viewModel.expandAdvancedSettings ? View.VISIBLE : View.GONE, default=gone}" /> + + + + + + + @@ -20,72 +23,71 @@ - - - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/sip_proxy_title"/> + + - - - - + app:layout_constraintTop_toBottomOf="@id/outbound_proxy_title"/> + app:layout_constraintTop_toBottomOf="@id/outbound_proxy" /> + @@ -310,6 +313,7 @@ android:visibility="@{viewModel.expandAdvancedSettings ? View.VISIBLE : View.GONE}" layout="@layout/account_advanced_settings" bind:updatePasswordClickListener="@{updatePasswordClickListener}" + bind:outboundProxyTooltipClickListener="@{outboundProxyTooltipClickListener}" bind:viewModel="@{viewModel}"/> diff --git a/app/src/main/res/layout/assistant_third_party_sip_account_login_fragment.xml b/app/src/main/res/layout/assistant_third_party_sip_account_login_fragment.xml index 43bcfe5746..e1654d3e45 100644 --- a/app/src/main/res/layout/assistant_third_party_sip_account_login_fragment.xml +++ b/app/src/main/res/layout/assistant_third_party_sip_account_login_fragment.xml @@ -9,6 +9,9 @@ + @@ -261,7 +264,7 @@ android:id="@+id/advanced_settings_group" android:layout_width="wrap_content" android:layout_height="wrap_content" - app:constraint_referenced_ids="authentication_id_label, authentication_id, outbound_proxy_label, outbound_proxy" + app:constraint_referenced_ids="authentication_id_label, authentication_id, proxy_label, proxy, outbound_proxy_label, outbound_proxy, outbound_proxy_tooltip" android:visibility="@{viewModel.expandAdvancedSettings ? View.VISIBLE : View.GONE, default=gone}" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6f50c63c4f..4721f2293b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -357,13 +357,15 @@ Dernière connexion : Se déconnecter du compte ? Si vous souhaitez supprimer définitivement votre compte rendez-vous sur : https://sip.linphone.org + Proxy SIP sortant + Si ce champ est rempli, l\'outbound proxy sera activé automatiquement. Laissez-le vide pour le désactiver. Paramètres de compte Activer les notifications push Notifications push non disponibles Chiffrement obligatoire des conversations - URL du serveur mandataire - Serveur mandataire sortant + Registrar URI + URI du proxy SIP sortant Paramètres de politique NAT URL du serveur STUN/TURN Activer ICE diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 953fd9ac75..5f02db8168 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -399,13 +399,15 @@ Last connection: Sign out of your account? If you wish to delete your account permanently, go to: https://sip.linphone.org + Outbound SIP Proxy + If this field is filled, the outbound proxy will be enabled automatically. Leave it empty to disable it. Account settings Allow push notifications Push notifications aren\'t available! IM encryption mandatory - SIP proxy server URL - Outbound proxy + Registrar URI + Outbound SIP Proxy URI NAT policy settings STUN/TURN server URL Enable ICE From 1d28ce184637839c80375adcb7f1a8fdefed12e0 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 6 Oct 2025 10:28:23 +0200 Subject: [PATCH 309/593] Added logs to help troubleshoot contact matching issue --- app/src/main/java/org/linphone/contacts/ContactsManager.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 46f360b9cd..d9a4787348 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -434,8 +434,10 @@ class ContactsManager @WorkerThread fun findContactByAddress(address: Address): Friend? { + Log.i("$TAG Looking for friend matching SIP address [${address.asStringUriOnly()}]") val found = coreContext.core.findFriend(address) if (found != null) { + Log.i("$TAG Found friend [${found.name}] matching SIP address [${address.asStringUriOnly()}]") return found } @@ -464,8 +466,11 @@ class ContactsManager } return if (!username.isNullOrEmpty() && (username.startsWith("+") || username.isDigitsOnly())) { - Log.d("$TAG Looking for friend with phone number [$username]") + Log.i("$TAG Looking for friend using phone number [$username]") val foundUsingPhoneNumber = coreContext.core.findFriendByPhoneNumber(username) + if (foundUsingPhoneNumber != null) { + Log.i("$TAG Found friend [${foundUsingPhoneNumber.name}] matching phone number [$username]") + } foundUsingPhoneNumber } else { null From d16dbcf0fdccfbd40ca343ee7243a9fb0a820f5e Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 6 Oct 2025 10:58:37 +0200 Subject: [PATCH 310/593] Fixed proximity sensor not turned ON when call is answered from notification --- .../java/org/linphone/core/CoreContext.kt | 41 +++++++++++++++++++ .../java/org/linphone/ui/call/CallActivity.kt | 31 ++------------ 2 files changed, 44 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index e89e6d02d5..487e9b42e4 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -23,6 +23,7 @@ import android.annotation.SuppressLint import android.app.Application import android.app.PendingIntent import android.content.Context +import android.content.Context.POWER_SERVICE import android.content.Intent import android.media.AudioDeviceCallback import android.media.AudioDeviceInfo @@ -30,6 +31,7 @@ import android.media.AudioManager import android.os.Handler import android.os.HandlerThread import android.os.Looper +import android.os.PowerManager import android.provider.Settings import android.provider.Settings.SettingNotFoundException import androidx.annotation.AnyThread @@ -131,6 +133,8 @@ class CoreContext private var keepAliveServiceStarted = false + private lateinit var proximityWakeLock: PowerManager.WakeLock + @SuppressLint("HandlerLeak") private lateinit var coreThread: Handler @@ -357,6 +361,15 @@ class CoreContext call.startRecording() } } + + if (core.isInBackground) { + // App is in background which means user likely answered the call from the notification + // In this case start proximity sensor, otherwise CallActivity will handle it + postOnMainThread { + Log.i("$TAG App is in background, start proximity sensor") + enableProximitySensor(true) + } + } } } Call.State.Error -> { @@ -407,6 +420,11 @@ class CoreContext core.videoDevice = frontFacing } } + + postOnMainThread { + Log.i("$TAG Releasing proximity sensor if it was enabled") + enableProximitySensor(false) + } } @WorkerThread @@ -670,6 +688,16 @@ class CoreContext Log.w("$TAG Keep alive service is enabled but auto start isn't and app is not in foreground, not starting it") } } + + val powerManager = context.getSystemService(POWER_SERVICE) as PowerManager + if (!powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { + Log.w("$TAG PROXIMITY_SCREEN_OFF_WAKE_LOCK isn't supported on this device!") + } else { + proximityWakeLock = powerManager.newWakeLock( + PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, + "${context.packageName};proximity_sensor" + ) + } } @WorkerThread @@ -1205,4 +1233,17 @@ class CoreContext fun updateCrashlyticsEnabledSetting(enabled: Boolean) { crashlyticsEnabled = enabled } + + @UiThread + fun enableProximitySensor(enable: Boolean) { + if (::proximityWakeLock.isInitialized) { + if (enable && !proximityWakeLock.isHeld) { + Log.i("$TAG Acquiring proximity sensor wake lock for 2 hours") + proximityWakeLock.acquire(7200 * 1000L) // 2 hours + } else if (!enable && proximityWakeLock.isHeld) { + Log.i("$TAG Releasing proximity sensor wake lock") + proximityWakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY) + } + } + } } diff --git a/app/src/main/java/org/linphone/ui/call/CallActivity.kt b/app/src/main/java/org/linphone/ui/call/CallActivity.kt index f4523570db..4a0ff2b28c 100644 --- a/app/src/main/java/org/linphone/ui/call/CallActivity.kt +++ b/app/src/main/java/org/linphone/ui/call/CallActivity.kt @@ -25,7 +25,6 @@ import android.content.pm.PackageManager import android.content.res.Resources import android.graphics.Color import android.os.Bundle -import android.os.PowerManager import androidx.activity.SystemBarStyle import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts @@ -80,8 +79,6 @@ class CallActivity : GenericActivity() { private lateinit var callsViewModel: CallsViewModel private lateinit var callViewModel: CurrentCallViewModel - private lateinit var proximityWakeLock: PowerManager.WakeLock - private var bottomSheetDialog: BottomSheetDialogFragment? = null private var isPipSupported = false @@ -150,16 +147,6 @@ class CallActivity : GenericActivity() { WindowInsetsCompat.CONSUMED } - val powerManager = getSystemService(POWER_SERVICE) as PowerManager - if (!powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { - Log.w("$TAG PROXIMITY_SCREEN_OFF_WAKE_LOCK isn't supported on this device!") - } - - proximityWakeLock = powerManager.newWakeLock( - PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, - "$packageName;proximity_sensor" - ) - lifecycleScope.launch(Dispatchers.Main) { WindowInfoTracker .getOrCreate(this@CallActivity) @@ -269,7 +256,7 @@ class CallActivity : GenericActivity() { callViewModel.proximitySensorEnabled.observe(this) { enabled -> Log.i("$TAG ${if (enabled) "Enabling" else "Disabling"} proximity sensor") - enableProximitySensor(enabled) + coreContext.enableProximitySensor(enabled) } callsViewModel.showIncomingCallEvent.observe(this) { @@ -374,7 +361,7 @@ class CallActivity : GenericActivity() { } override fun onPause() { - enableProximitySensor(false) + coreContext.enableProximitySensor(false) super.onPause() @@ -383,7 +370,7 @@ class CallActivity : GenericActivity() { } override fun onDestroy() { - enableProximitySensor(false) + coreContext.enableProximitySensor(false) super.onDestroy() @@ -545,16 +532,4 @@ class CallActivity : GenericActivity() { modalBottomSheet.show(supportFragmentManager, ConferenceLayoutMenuDialogFragment.TAG) bottomSheetDialog = modalBottomSheet } - - private fun enableProximitySensor(enable: Boolean) { - if (enable && !proximityWakeLock.isHeld) { - Log.i("$TAG Acquiring PROXIMITY_SCREEN_OFF_WAKE_LOCK for 2 hours") - proximityWakeLock.acquire(7200 * 1000L) // 2 heures - } else if (!enable && proximityWakeLock.isHeld) { - Log.i( - "$TAG Asking to release PROXIMITY_SCREEN_OFF_WAKE_LOCK (next time sensor detects no proximity)" - ) - proximityWakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY) - } - } } From 7e2527c46c78bc7bf198c323ad2011d0d217db95 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 6 Oct 2025 16:19:43 +0200 Subject: [PATCH 311/593] Fixed wrong label for LDAP form field --- app/src/main/res/values/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5f02db8168..eeb9869268 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -290,8 +290,8 @@ Bind DN Password Use TLS - Search - Search base (can\'t be empty) + Search base (can\'t be empty) + Filter Max results Timeout (in seconds) Delay between two queries (in milliseconds) From 7855d4e1db227f53a284bab5757a7a9ea8f5cc34 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 13 Oct 2025 09:34:26 +0200 Subject: [PATCH 312/593] Updated lock open icon & warning color to increase contrast --- .../main/res/drawable/lock_simple_open.xml | 19 +++++++++++++++---- .../res/layout/chat_conversation_fragment.xml | 4 ++-- ...n_send_area_disabled_unsecured_warning.xml | 4 ++-- .../chat_conversation_unsafe_first_event.xml | 4 ++-- app/src/main/res/layout/chat_list_cell.xml | 2 +- app/src/main/res/values-night/themes.xml | 1 + app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/themes.xml | 1 + 9 files changed, 26 insertions(+), 11 deletions(-) diff --git a/app/src/main/res/drawable/lock_simple_open.xml b/app/src/main/res/drawable/lock_simple_open.xml index 34d68ddeb6..0bf8456789 100644 --- a/app/src/main/res/drawable/lock_simple_open.xml +++ b/app/src/main/res/drawable/lock_simple_open.xml @@ -1,9 +1,20 @@ + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M19.5,8.25L4.5,8.25C4.086,8.25 3.75,8.586 3.75,9L3.75,19.5C3.75,19.914 4.086,20.25 4.5,20.25L19.5,20.25C19.914,20.25 20.25,19.914 20.25,19.5L20.25,9C20.25,8.586 19.914,8.25 19.5,8.25ZM19.5,8.25" + android:strokeLineJoin="round" + android:strokeWidth="1.5" + android:fillColor="#00000000" + android:strokeColor="#4e6074" + android:strokeLineCap="round"/> + diff --git a/app/src/main/res/layout/chat_conversation_fragment.xml b/app/src/main/res/layout/chat_conversation_fragment.xml index 11713365b9..580039cfc9 100644 --- a/app/src/main/res/layout/chat_conversation_fragment.xml +++ b/app/src/main/res/layout/chat_conversation_fragment.xml @@ -146,7 +146,7 @@ app:layout_constraintEnd_toStartOf="@id/unsecure_label" app:layout_constraintTop_toBottomOf="@id/title" app:layout_constraintBottom_toBottomOf="@id/avatar" - app:tint="?attr/color_warning_600"/> + app:tint="?attr/color_plain_text_security_warning_600"/> @@ -49,7 +49,7 @@ android:layout_marginEnd="15dp" android:text="@string/conversation_warning_disabled_because_not_secured_title" android:textSize="12sp" - android:textColor="@color/orange_warning_600" + android:textColor="?attr/color_plain_text_security_warning_600" app:layout_constraintStart_toEndOf="@id/disabled_icon" app:layout_constraintEnd_toEndOf="@id/disabled_background" app:layout_constraintTop_toTopOf="parent"/> diff --git a/app/src/main/res/layout/chat_conversation_unsafe_first_event.xml b/app/src/main/res/layout/chat_conversation_unsafe_first_event.xml index 0901cda21a..16ec2b6c9b 100644 --- a/app/src/main/res/layout/chat_conversation_unsafe_first_event.xml +++ b/app/src/main/res/layout/chat_conversation_unsafe_first_event.xml @@ -34,7 +34,7 @@ android:adjustViewBounds="true" android:contentDescription="@null" android:src="@drawable/lock_simple_open" - app:tint="@color/orange_warning_600" + app:tint="?attr/color_plain_text_security_warning_600" app:layout_constraintTop_toTopOf="@id/unsafe_title" app:layout_constraintBottom_toBottomOf="@id/unsafe_subtitle" app:layout_constraintStart_toStartOf="@id/unsafe_background"/> @@ -49,7 +49,7 @@ android:layout_marginEnd="15dp" android:text="@string/conversation_warning_disabled_because_not_secured_title" android:textSize="12sp" - android:textColor="@color/orange_warning_600" + android:textColor="?attr/color_plain_text_security_warning_600" app:layout_constraintStart_toEndOf="@id/unsafe_icon" app:layout_constraintEnd_toEndOf="@id/unsafe_background" app:layout_constraintTop_toTopOf="parent"/> diff --git a/app/src/main/res/layout/chat_list_cell.xml b/app/src/main/res/layout/chat_list_cell.xml index 746c81f1dd..fb6d4823ae 100644 --- a/app/src/main/res/layout/chat_list_cell.xml +++ b/app/src/main/res/layout/chat_list_cell.xml @@ -207,7 +207,7 @@ app:layout_constraintTop_toBottomOf="@id/date_time" app:layout_constraintBottom_toTopOf="@id/separator" app:layout_constraintEnd_toStartOf="@id/ephemeral" - app:tint="?attr/color_warning_600" /> + app:tint="?attr/color_plain_text_security_warning_600" /> @color/blue_info_500_night @color/orange_warning_600_night + @color/orange_plain_text_security_warning_600 @color/bc_white @color/background_color_alt_dark_mode diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index ea34103d9e..cf81fc54c0 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -48,6 +48,7 @@ + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 7b931aeaa6..98036be3c3 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -54,6 +54,7 @@ #4AA8FF #64B4FF + #AF9308 #DBB820 #E1C133 diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index b19b6bd3eb..fae1e19df2 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -51,6 +51,7 @@ @color/blue_info_500 @color/orange_warning_600 + @color/orange_plain_text_security_warning_600 @color/bc_white @color/bc_white From b71249ea36dcfee90aaca817a425df3eb8eee4d7 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 13 Oct 2025 09:40:11 +0200 Subject: [PATCH 313/593] Fixed typos in French translation --- app/src/main/res/values-fr/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4721f2293b..ee5db94dd5 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -382,8 +382,8 @@ Utiliser CPIM dans les conversations \"basiques\" URI de la messagerie vocale URI du serveur MWI (Message Waiting Indicator) - Formatter les numéros en utilisant l\'indicatif international - Remplacer + par 00 lors du formattage des numéros de téléphone + Formater les numéros en utilisant l\'indicatif international + Remplacer + par 00 lors du formatage des numéros de téléphone Mettre à jour le mot de passe Autentification requise From d694789d4b1fabd51c3c5c3a977f3dd5f2d1b288 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 6 Oct 2025 14:04:19 +0200 Subject: [PATCH 314/593] Trying to troubleshoot missing participant video when changing conference layout sometimes --- .../conference/model/ConferenceParticipantDeviceModel.kt | 7 +++++++ .../ui/call/conference/viewmodel/ConferenceViewModel.kt | 3 +++ 2 files changed, 10 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/call/conference/model/ConferenceParticipantDeviceModel.kt b/app/src/main/java/org/linphone/ui/call/conference/model/ConferenceParticipantDeviceModel.kt index e4c584feed..2d9d937ae7 100644 --- a/app/src/main/java/org/linphone/ui/call/conference/model/ConferenceParticipantDeviceModel.kt +++ b/app/src/main/java/org/linphone/ui/call/conference/model/ConferenceParticipantDeviceModel.kt @@ -192,9 +192,16 @@ class ConferenceParticipantDeviceModel @WorkerThread fun destroy() { + clearWindowId() device.removeListener(deviceListener) } + @WorkerThread + fun clearWindowId() { + Log.i("$TAG Clearing participant [${device.address.asStringUriOnly()}] device window ID") + device.nativeVideoWindowId = null + } + @UiThread fun setTextureView(view: TextureView) { Log.i( diff --git a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt index e6b1ac40d2..be868a14e3 100644 --- a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt @@ -463,6 +463,9 @@ class ConferenceViewModel } } + Log.i("$TAG Clearing participant devices window IDs") + participantDevices.value.orEmpty().forEach(ConferenceParticipantDeviceModel::clearWindowId) + if (currentLayout == AUDIO_ONLY_LAYOUT) { // Previous layout was audio only, make sure video isn't sent without user consent when switching layout Log.i( From 5ee3ba4ea9d07f891806637eb75864e40e33b67b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 13 Oct 2025 11:55:33 +0200 Subject: [PATCH 315/593] Updated warning about conversations that aren't E2E encrypted --- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values/strings.xml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ee5db94dd5..ad5d197fe1 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -521,7 +521,7 @@ Les messages de cette conversation sont chiffrés de bout en bout. Seul votre correspondant peut les déchiffrer. La confidentialité de vos échanges garantie Grâce à la technologie de chiffrement de bout en bout de &appName;, la confidentialité de vos messages, appels et réunions avec vos correspondants est garantie. Personne ne pourra déchiffrer vos conversations, pas même &appName;. - Les messages ne sont pas chiffrés, assurez-vous de ne pas partager d\'informations sensibles ! + Les messages ne sont pas chiffrés de bout en bout, assurez-vous de ne pas partager d\'informations sensibles ! Conversation non chiffrée Les messages échangés dans cette conversation peuvent être interceptés et consultés par des personnes autres que le destinataire désiré, la confidentialité n\'est pas garantie ! Cette conversation n\'est pas chiffrée ! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eeb9869268..2349d76579 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -419,7 +419,7 @@ Conference factory URI Audio/video conference factory URI CCMP server URL - E2E encryption keys server URL + End-to-end encryption keys server URL Bundle mode Use CPIM in \"basic\" conversations Voicemail URI @@ -561,10 +561,10 @@ No media found… No document found… End-to-end encrypted conversation - Messages in this conversation are e2e encrypted. Only your correspondent can decrypt them. + Messages in this conversation are end-to-end encrypted. Only your correspondent can decrypt them. Guaranteed confidentiality Thanks to end-to-end encryption technology in &appName;, messages, calls and meetings confidentiality are guaranteed. No-one can decrypt exchanged data, not even ourselves. - Messages aren\'t encrypted, make sure you don\'t share sensitive information! + Messages aren\'t end-to-end encrypted, make sure you don\'t share sensitive information! Unencrypted conversation Messages exchanged in this conversation can be intercepted and read by other people than your correspondent, confidentiality is not guaranteed! This conversation is not encrypted! From d9ab84057035b38b264b423bb1cedc417eb5a317 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 14 Oct 2025 14:23:41 +0200 Subject: [PATCH 316/593] Prevent black screen when trying to scan a QR code in assistant right after granting the app the CAMERA permission (on some devices) --- .../org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt index de9cd4f8f7..6f986194e3 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt @@ -118,6 +118,10 @@ class QrCodeViewModel @UiThread fun setBackCamera() { coreContext.postOnCoreThread { core -> + // Just in case, on some devices such as Xiaomi Redmi Note 5 + // this is required right after granting the CAMERA permission + core.reloadVideoDevices() + for (camera in core.videoDevicesList) { if (camera.contains("Back")) { Log.i("$TAG Found back facing camera [$camera], using it") From d75c48cd34bfba114d8f00c8636bee5087fc8318 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 15 Oct 2025 09:58:55 +0200 Subject: [PATCH 317/593] Fixed misleading method name --- .../linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt | 2 +- app/src/main/res/layout-sw600dp/assistant_register_fragment.xml | 2 +- app/src/main/res/layout/assistant_register_fragment.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt index 912e339670..a2824578a3 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt @@ -308,7 +308,7 @@ class AccountCreationViewModel } @UiThread - fun phoneNumberConfirmedByUser() { + fun askUserToConfirmPhoneNumber() { coreContext.postOnCoreThread { if (::accountManagerServices.isInitialized) { val dialPlan = selectedDialPlan.value diff --git a/app/src/main/res/layout-sw600dp/assistant_register_fragment.xml b/app/src/main/res/layout-sw600dp/assistant_register_fragment.xml index c3a301d9f0..298f6d9ff7 100644 --- a/app/src/main/res/layout-sw600dp/assistant_register_fragment.xml +++ b/app/src/main/res/layout-sw600dp/assistant_register_fragment.xml @@ -351,7 +351,7 @@ app:layout_constraintBottom_toBottomOf="@id/password" /> Date: Wed, 15 Oct 2025 11:02:38 +0200 Subject: [PATCH 318/593] Bumped firebase & gms version --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dcfc7f701f..52647c3646 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] agp = "8.13.0" kotlin = "2.2.20" -gmsGoogleServices = "4.4.3" +gmsGoogleServices = "4.4.4" firebaseCrashlytics = "3.0.6" -firebaseBomVersion = "34.3.0" +firebaseBomVersion = "34.4.0" #noinspection NewerVersionAvailable ktlint = "12.3.0" From e2c9e1196f0ca240cc4640cb5469606e50f86c2a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 15 Oct 2025 12:10:28 +0200 Subject: [PATCH 319/593] Force all LDAP fields to be filled, added verbose mode toggle --- .../main/settings/viewmodel/LdapViewModel.kt | 57 ++++++++++++++----- .../res/layout/settings_contacts_ldap.xml | 51 +++++++++++++---- app/src/main/res/values-fr/strings.xml | 3 +- app/src/main/res/values/strings.xml | 3 +- 4 files changed, 86 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/LdapViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/LdapViewModel.kt index 407522b030..11bc1b22b0 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/LdapViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/LdapViewModel.kt @@ -65,6 +65,8 @@ class LdapViewModel : GenericViewModel() { val sipDomain = MutableLiveData() + val verboseMode = MutableLiveData() + val ldapServerOperationSuccessfulEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -80,6 +82,7 @@ class LdapViewModel : GenericViewModel() { minCharacters.value = "3" requestTimeout.value = "5" requestDelay.value = "2000" + verboseMode.value = true } @UiThread @@ -110,6 +113,7 @@ class LdapViewModel : GenericViewModel() { nameAttributes.postValue(ldapParams.nameAttribute.orEmpty()) sipAttributes.postValue(ldapParams.sipAttribute.orEmpty()) sipDomain.postValue(ldapParams.sipDomain.orEmpty()) + verboseMode.postValue(ldapParams.debugLevel == Ldap.DebugLevel.Verbose) Log.i("$TAG Existing LDAP server values loaded") } } @@ -141,36 +145,59 @@ class LdapViewModel : GenericViewModel() { useTls.value = useTls.value == false } + @UiThread + fun toggleDebug() { + verboseMode.value = verboseMode.value == false + } + @UiThread fun addServer() { coreContext.postOnCoreThread { core -> try { val server = serverUrl.value.orEmpty().trim() - if (server.isEmpty()) { - Log.e("$TAG Server field can't be empty!") - showRedToast(R.string.settings_contacts_ldap_empty_server_error_toast, R.drawable.warning_circle) + val bindDn = bindDn.value.orEmpty().trim() + val base = searchBase.value.orEmpty().trim() + val filter = searchFilter.value.orEmpty().trim() + val maxResults = maxResults.value.orEmpty().trim() + val timeout = requestTimeout.value.orEmpty().trim() + val delay = requestDelay.value.orEmpty().trim() + val minChars = minCharacters.value.orEmpty().trim() + val nameAttrs = nameAttributes.value.orEmpty().trim() + val sipAttrs = sipAttributes.value.orEmpty().trim() + val sipDomain = sipDomain.value.orEmpty().trim() + if ( + server.isEmpty() || bindDn.isEmpty() || base.isEmpty() || filter.isEmpty() || + maxResults.isEmpty() || timeout.isEmpty() || delay.isEmpty() || + minChars.isEmpty() || nameAttrs.isEmpty() || sipAttrs.isEmpty() || + sipDomain.isEmpty() + ) { + Log.e("$TAG All fields must be filled!") + showRedToast(R.string.settings_contacts_ldap_empty_field_error_toast, R.drawable.warning_circle) return@postOnCoreThread } val ldapParams = core.createLdapParams() - ldapParams.enabled = isEnabled.value == true ldapParams.server = server - ldapParams.bindDn = bindDn.value.orEmpty().trim() + ldapParams.bindDn = bindDn ldapParams.password = password.value.orEmpty().trim() ldapParams.authMethod = Ldap.AuthMethod.Simple ldapParams.isTlsEnabled = useTls.value == true ldapParams.serverCertificatesVerificationMode = Ldap.CertVerificationMode.Default - ldapParams.baseObject = searchBase.value.orEmpty().trim() - ldapParams.filter = searchFilter.value.orEmpty().trim() - ldapParams.maxResults = maxResults.value.orEmpty().trim().toInt() - ldapParams.timeout = requestTimeout.value.orEmpty().trim().toInt() - ldapParams.delay = requestDelay.value.orEmpty().trim().toInt() - ldapParams.minChars = minCharacters.value.orEmpty().trim().toInt() - ldapParams.nameAttribute = nameAttributes.value.orEmpty().trim() - ldapParams.sipAttribute = sipAttributes.value.orEmpty().trim() - ldapParams.sipDomain = sipDomain.value.orEmpty().trim() - ldapParams.debugLevel = Ldap.DebugLevel.Verbose + ldapParams.baseObject = base + ldapParams.filter = filter + ldapParams.maxResults = maxResults.toInt() + ldapParams.timeout = timeout.toInt() + ldapParams.delay = delay.toInt() + ldapParams.minChars = minChars.toInt() + ldapParams.nameAttribute = nameAttrs + ldapParams.sipAttribute = sipAttrs + ldapParams.sipDomain = sipDomain + ldapParams.debugLevel = if (verboseMode.value == true) { + Ldap.DebugLevel.Verbose + } else { + Ldap.DebugLevel.Off + } if (isEdit.value == true && ::ldapToEdit.isInitialized) { ldapToEdit.params = ldapParams diff --git a/app/src/main/res/layout/settings_contacts_ldap.xml b/app/src/main/res/layout/settings_contacts_ldap.xml index 139a455243..5fcad6d624 100644 --- a/app/src/main/res/layout/settings_contacts_ldap.xml +++ b/app/src/main/res/layout/settings_contacts_ldap.xml @@ -162,7 +162,7 @@ android:paddingStart="20dp" android:paddingEnd="20dp" android:text="@={viewModel.bindDn}" - android:inputType="text" + android:inputType="text|textNoSuggestions" android:hint="@string/settings_contacts_ldap_bind_dn_title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -244,6 +244,35 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/password" /> + + + + @@ -271,7 +300,7 @@ android:paddingStart="20dp" android:paddingEnd="20dp" android:text="@={viewModel.searchBase}" - android:inputType="text" + android:inputType="text|textNoSuggestions" android:hint="@string/settings_contacts_ldap_search_base_title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -304,7 +333,7 @@ android:paddingStart="20dp" android:paddingEnd="20dp" android:text="@={viewModel.searchFilter}" - android:inputType="text" + android:inputType="text|textNoSuggestions" android:hint="@string/settings_contacts_ldap_search_filter_title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -337,7 +366,7 @@ android:paddingStart="20dp" android:paddingEnd="20dp" android:text="@={viewModel.maxResults}" - android:inputType="numberSigned" + android:inputType="number|numberSigned" android:hint="@string/settings_contacts_ldap_max_results_title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -370,7 +399,7 @@ android:paddingStart="20dp" android:paddingEnd="20dp" android:text="@={viewModel.requestTimeout}" - android:inputType="numberSigned" + android:inputType="number|numberSigned" android:hint="@string/settings_contacts_ldap_request_timeout_title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -403,7 +432,7 @@ android:paddingStart="20dp" android:paddingEnd="20dp" android:text="@={viewModel.requestDelay}" - android:inputType="numberSigned" + android:inputType="number|numberSigned" android:hint="@string/settings_contacts_ldap_request_delay_title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -436,7 +465,7 @@ android:paddingStart="20dp" android:paddingEnd="20dp" android:text="@={viewModel.minCharacters}" - android:inputType="numberSigned" + android:inputType="number|numberSigned" android:hint="@string/settings_contacts_ldap_min_characters_title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -469,7 +498,7 @@ android:paddingStart="20dp" android:paddingEnd="20dp" android:text="@={viewModel.nameAttributes}" - android:inputType="text" + android:inputType="text|textNoSuggestions" android:hint="@string/settings_contacts_ldap_name_attributes_title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -502,7 +531,7 @@ android:paddingStart="20dp" android:paddingEnd="20dp" android:text="@={viewModel.sipAttributes}" - android:inputType="text" + android:inputType="text|textNoSuggestions" android:hint="@string/settings_contacts_ldap_sip_attributes_title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -536,7 +565,7 @@ android:paddingStart="20dp" android:paddingEnd="20dp" android:text="@={viewModel.sipDomain}" - android:inputType="text" + android:inputType="text|textNoSuggestions" android:hint="@string/settings_contacts_ldap_sip_domain_title" app:layout_constraintVertical_bias="0" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ad5d197fe1..835d8c85f0 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -248,6 +248,7 @@ Bind DN Mot de passe Utiliser TLS + Ajouter les logs LDAP à ceux de &appName; Base de recherche (ne peut être vide) Filtre Nombre de résultats maximum @@ -258,7 +259,7 @@ Attributs SIP Domaine SIP Une erreur s\'est produite, la configuration LDAP n\'a pas été sauvegardée ! - L\'URL du serveur ne peut être vide + Tous les champs doivent être remplis Réunions Disposition par défaut Intervenant actif diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2349d76579..2794485d63 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -290,6 +290,7 @@ Bind DN Password Use TLS + Add LDAP logs to &appName; ones Search base (can\'t be empty) Filter Max results @@ -300,7 +301,7 @@ SIP attributes SIP domain A error occurred, LDAP server not saved! - Server URL can\'t be empty + All fields must be filled Meetings Default layout Active speaker From 069997d780047b127f8ca85a8ce00b315e883952 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 15 Oct 2025 16:20:48 +0200 Subject: [PATCH 320/593] Fixed displayer screen sharing participant name --- .../viewmodel/ConferenceViewModel.kt | 18 ++++++++++++++++++ ...call_conference_active_speaker_fragment.xml | 2 +- ...call_conference_active_speaker_fragment.xml | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt index be868a14e3..bee37298d2 100644 --- a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt @@ -68,6 +68,8 @@ class ConferenceViewModel val conferenceLayout = MutableLiveData() + val screenSharingParticipantName = MutableLiveData() + val isScreenSharing = MutableLiveData() val isPaused = MutableLiveData() @@ -240,7 +242,17 @@ class ConferenceViewModel "$TAG Participant device [${device.address.asStringUriOnly()}] is ${if (enabled) "sharing it's screen" else "no longer sharing it's screen"}" ) isScreenSharing.postValue(enabled) + if (enabled) { + val deviceModel = participantDevices.value.orEmpty().find { + it.device == device || device.address.weakEqual(it.device.address) + } + if (deviceModel != null) { + screenSharingParticipantName.postValue(deviceModel.name) + } else { + Log.w("$TAG Failed to find screen sharing participant device model!") + } + val call = conference.call if (call != null) { val currentLayout = getCurrentLayout(call) @@ -253,6 +265,8 @@ class ConferenceViewModel } else { Log.e("$TAG Screen sharing was enabled but conference's call is null!") } + } else { + screenSharingParticipantName.postValue("") } } @@ -577,6 +591,10 @@ class ConferenceViewModel activeSpeaker.postValue(model) activeSpeakerParticipantDeviceFound = true } + if (device == conference.screenSharingParticipantDevice) { + Log.i("$TAG Using participant is [${model.name}] as current screen sharing sender") + screenSharingParticipantName.postValue(model.name) + } } } } diff --git a/app/src/main/res/layout-land/call_conference_active_speaker_fragment.xml b/app/src/main/res/layout-land/call_conference_active_speaker_fragment.xml index 0744c8de89..04aca49bc1 100644 --- a/app/src/main/res/layout-land/call_conference_active_speaker_fragment.xml +++ b/app/src/main/res/layout-land/call_conference_active_speaker_fragment.xml @@ -110,7 +110,7 @@ android:layout_height="wrap_content" android:layout_marginStart="10dp" android:layout_marginBottom="10dp" - android:text="@{conferenceViewModel.activeSpeaker.name, default=`John Doe`}" + android:text="@{conferenceViewModel.isScreenSharing ? conferenceViewModel.screenSharingParticipantName : conferenceViewModel.activeSpeaker.name, default=`John Doe`}" android:textColor="@color/bc_white" android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/call_conference_active_speaker_fragment.xml b/app/src/main/res/layout/call_conference_active_speaker_fragment.xml index 3d25fa6a07..2eff858942 100644 --- a/app/src/main/res/layout/call_conference_active_speaker_fragment.xml +++ b/app/src/main/res/layout/call_conference_active_speaker_fragment.xml @@ -109,7 +109,7 @@ android:layout_height="wrap_content" android:layout_marginStart="10dp" android:layout_marginBottom="10dp" - android:text="@{conferenceViewModel.activeSpeaker.name, default=`John Doe`}" + android:text="@{conferenceViewModel.isScreenSharing ? conferenceViewModel.screenSharingParticipantName : conferenceViewModel.activeSpeaker.name, default=`John Doe`}" android:textColor="@color/bc_white" android:textSize="20sp" app:layout_constraintBottom_toTopOf="@id/active_speaker_miniatures_horizontal_layout" From 45c756cfd63636661a55e6bde24c1a59a35051fe Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 16 Oct 2025 11:01:31 +0200 Subject: [PATCH 321/593] Updated CHANGELOG & version code from release/6.0 branch --- CHANGELOG.md | 22 ++++++++++++++++++++++ app/build.gradle.kts | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6accff7adb..a3a2dafcdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,28 @@ Group changes to describe their impact on the project, as follows: - Increased shared media preview size in chat - Un-encrypted conversation warning will be more visible for accounts that support end-to-end encrypted conversations - Made numpad buttons larger by changing their shape +- All LDAP fields are mandatory now, added toggle to choose wether or not to print LDAP logs +- Permission fragment will only show missing ones + +## [6.0.19] - 2025-10-16 + +### Added +- Spanish and Slovakian translations thanks to Weblate contributors + +### Changed +- SIP addresses domain hidden in Suggestions if it matches the currently selected account SIP identity domain +- Start proximity sensor when an incoming call is answered from the notification (disabling screen when device is near) + +### Fixed +- Black screen when trying to scan a QR Code right after granting CAMERA permission (only happened on some devices) +- Possible crash due to ConcurrentModificationException +- Camera preview in conference that was black sometimes after switching layout +- Possibly wrong screen sharing participant name in conference +- Presence SUBSCRIBE that was only sent for sip.linphone.org accounts +- Keyboard suggestions in participant picker textfield +- Account labelled as Disabled instead of Disconnected when network isn't reachable +- Suggestions generated avatar if username starts with '+' +- Two LDAP fields label where swapped ## [6.0.18] - 2025-09-15 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 23e6b4f478..d77c8ed368 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -100,7 +100,7 @@ android { applicationId = packageName minSdk = 28 targetSdk = 36 - versionCode = 600018 // 6.00.018 + versionCode = 600019 // 6.00.019 versionName = "6.1.0-alpha" manifestPlaceholders["appAuthRedirectScheme"] = packageName From 4dc1b9a903867ab355cc339ea2bb3c59c168bb3f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 20 Oct 2025 10:00:53 +0200 Subject: [PATCH 322/593] Disable camera button while in conference with audio_only layout --- app/src/main/res/layout/call_actions_generic.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/call_actions_generic.xml b/app/src/main/res/layout/call_actions_generic.xml index 0dcc1ab567..b1cfe50dd1 100644 --- a/app/src/main/res/layout/call_actions_generic.xml +++ b/app/src/main/res/layout/call_actions_generic.xml @@ -4,6 +4,7 @@ + @@ -52,7 +53,7 @@ android:layout_height="@dimen/call_button_size" android:layout_marginEnd="16dp" android:padding="@dimen/call_button_icon_padding" - android:enabled="@{!viewModel.isPaused && !viewModel.isPausedByRemote && !viewModel.videoUpdateInProgress}" + android:enabled="@{!viewModel.isPaused && !viewModel.isPausedByRemote && !viewModel.videoUpdateInProgress && (!viewModel.conferenceModel.isCurrentCallInConference || viewModel.conferenceModel.conferenceLayout != ConferenceViewModel.AUDIO_ONLY_LAYOUT)}" android:visibility="@{viewModel.hideVideo ? View.GONE : View.VISIBLE}" android:src="@{viewModel.isSendingVideo ? @drawable/video_camera : @drawable/video_camera_slash, default=@drawable/video_camera}" android:background="@drawable/in_call_button_background_red" From d74ccb523e06ce89458ddc4d4f7ceffa1043f4b4 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 20 Oct 2025 10:52:06 +0200 Subject: [PATCH 323/593] Prevent LDAP password to be removed after editing existing config --- .../linphone/ui/main/settings/viewmodel/LdapViewModel.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/LdapViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/LdapViewModel.kt index 11bc1b22b0..7e03643f2d 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/LdapViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/LdapViewModel.kt @@ -165,6 +165,7 @@ class LdapViewModel : GenericViewModel() { val nameAttrs = nameAttributes.value.orEmpty().trim() val sipAttrs = sipAttributes.value.orEmpty().trim() val sipDomain = sipDomain.value.orEmpty().trim() + val pwd = password.value.orEmpty().trim() if ( server.isEmpty() || bindDn.isEmpty() || base.isEmpty() || filter.isEmpty() || maxResults.isEmpty() || timeout.isEmpty() || delay.isEmpty() || @@ -180,7 +181,11 @@ class LdapViewModel : GenericViewModel() { ldapParams.enabled = isEnabled.value == true ldapParams.server = server ldapParams.bindDn = bindDn - ldapParams.password = password.value.orEmpty().trim() + if (!pwd.isEmpty()) { + ldapParams.password = pwd + } else if (::ldapToEdit.isInitialized) { + ldapParams.password = ldapToEdit.params.password + } ldapParams.authMethod = Ldap.AuthMethod.Simple ldapParams.isTlsEnabled = useTls.value == true ldapParams.serverCertificatesVerificationMode = Ldap.CertVerificationMode.Default From 0e71a726c15dcb7943675db4b12a369dff3f384f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 20 Oct 2025 11:29:48 +0200 Subject: [PATCH 324/593] Fixed infinite LDAP queries loop in case it returns a result that doesn't match the request --- .../org/linphone/contacts/ContactsManager.kt | 72 ++++++++++--------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index d9a4787348..33625bfc58 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -97,57 +97,63 @@ class ContactsManager override fun onSearchResultsReceived(magicSearch: MagicSearch) { reloadRemoteContactsJob?.cancel() + var queriedSipUri = "" + for ((key, value) in magicSearchMap.entries) { + if (value == magicSearch) { + queriedSipUri = key + } + } + val results = magicSearch.lastSearch - Log.i("$TAG [${results.size}] magic search results available") + Log.i( + "$TAG [${results.size}] magic search results available for query upon SIP URI [$queriedSipUri]" + ) var found = false if (results.isNotEmpty()) { - val result = results.first { - it.friend != null - } + val result = results.first { it.friend != null } if (result != null) { val friend = result.friend!! Log.i("$TAG Found matching friend in source [${result.sourceFlags}]") - found = true - - // Store friend in app's cache to be re-used in call history, conversations, etc... - val temporaryFriendList = getRemoteContactDirectoriesCacheFriendList() - temporaryFriendList.addFriend(friend) - newContactAdded(friend) - Log.i( - "$TAG Stored discovered friend [${friend.name}] in temporary friend list, for later use" - ) + val address = result.address?.asStringUriOnly().orEmpty() + if (address.isEmpty() || (queriedSipUri.isNotEmpty() && queriedSipUri != address)) { + Log.w("$TAG Received friend [${friend.name}] with SIP URI [$address] doesn't match queried SIP URI [$queriedSipUri]") + } else { + found = true + + // Store friend in app's cache to be re-used in call history, conversations, etc... + val temporaryFriendList = getRemoteContactDirectoriesCacheFriendList() + temporaryFriendList.addFriend(friend) + newContactAdded(friend) + Log.i( + "$TAG Stored discovered friend [${friend.name}] in temporary friend list, for later use" + ) - for (listener in listeners) { - listener.onContactFoundInRemoteDirectory(friend) - } + for (listener in listeners) { + listener.onContactFoundInRemoteDirectory(friend) + } - reloadRemoteContactsJob = coroutineScope.launch { - delay(DELAY_BEFORE_RELOADING_CONTACTS_AFTER_MAGIC_SEARCH_RESULT) - coreContext.postOnCoreThread { - Log.i("$TAG At least a new SIP address was discovered, reloading contacts") - conferenceAvatarMap.values.forEach(ContactAvatarModel::destroy) - conferenceAvatarMap.clear() + reloadRemoteContactsJob = coroutineScope.launch { + delay(DELAY_BEFORE_RELOADING_CONTACTS_AFTER_MAGIC_SEARCH_RESULT) + coreContext.postOnCoreThread { + Log.i("$TAG At least a new SIP address was discovered, reloading contacts") + conferenceAvatarMap.values.forEach(ContactAvatarModel::destroy) + conferenceAvatarMap.clear() - notifyContactsListChanged() + notifyContactsListChanged() + } } } } } - var foundKey = "" - for ((key, value) in magicSearchMap.entries) { - if (value == magicSearch) { - foundKey = key - } - } - if (foundKey.isNotEmpty()) { - magicSearchMap.remove(foundKey) + if (queriedSipUri.isNotEmpty()) { + magicSearchMap.remove(queriedSipUri) if (!found) { Log.i( - "$TAG SIP URI [$foundKey] wasn't found in remote directories, adding it to unknown list to prevent further queries" + "$TAG SIP URI [$queriedSipUri] wasn't found in remote directories, adding it to unknown list to prevent further queries" ) - unknownRemoteContactDirectoriesContactsMap.add(foundKey) + unknownRemoteContactDirectoriesContactsMap.add(queriedSipUri) } } magicSearch.removeListener(this) From b0283043ee73f186760255456ad7d3956bd26f6b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 21 Oct 2025 15:30:26 +0200 Subject: [PATCH 325/593] Save generated avatar as files in cache for faster re-user and lower memory footprint --- .../org/linphone/ui/main/chat/model/FileModel.kt | 7 +++++-- .../java/org/linphone/utils/DataBindingUtils.kt | 11 +---------- app/src/main/java/org/linphone/utils/FileUtils.kt | 14 +++++++++----- app/src/main/java/org/linphone/utils/ImageUtils.kt | 13 ++++++++++--- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt index 2d922731e0..95f3a173b0 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/FileModel.kt @@ -39,6 +39,7 @@ import org.linphone.utils.TimestampUtils import androidx.core.net.toUri import androidx.core.graphics.createBitmap import kotlinx.coroutines.withContext +import org.linphone.utils.FileUtils.Companion.getFileStorageCacheDir import java.io.File class FileModel @@ -199,7 +200,8 @@ class FileModel PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY ) - val previewPath = FileUtils.storeBitmap(previewBitmap, fileName) + val file = getFileStorageCacheDir("$fileName.jpg", true) + val previewPath = FileUtils.storeBitmap(previewBitmap, file) Log.i("$TAG Preview of PDF file [$path] available at [$previewPath]") mediaPreview.postValue(previewPath) mediaPreviewAvailable.postValue(true) @@ -226,7 +228,8 @@ class FileModel MediaStore.Images.Thumbnails.MINI_KIND ) if (previewBitmap != null) { - val previewPath = FileUtils.storeBitmap(previewBitmap, fileName) + val file = getFileStorageCacheDir("$fileName.jpg", true) + val previewPath = FileUtils.storeBitmap(previewBitmap, file) Log.i("$TAG Preview of video file [$path] available at [$previewPath]") mediaPreview.postValue(previewPath) mediaPreviewAvailable.postValue(true) diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 88088fb118..d8adb9ef11 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -64,7 +64,6 @@ import com.google.android.flexbox.FlexboxLayout import org.linphone.BR import org.linphone.R import org.linphone.contacts.AbstractAvatarModel -import org.linphone.contacts.AvatarGenerator import org.linphone.core.ConsolidatedPresence import org.linphone.core.tools.Log import org.linphone.ui.NotoSansFont @@ -449,14 +448,6 @@ fun ImageView.loadCallAvatarWithCoil(model: AbstractAvatarModel?) { loadContactPictureWithCoil(this, model, size = size, textSize = initialsSize) } -@UiThread -@BindingAdapter("coilInitials") -fun ImageView.loadInitialsAvatarWithCoil(initials: String?) { - val builder = AvatarGenerator(context) - builder.setInitials(initials.orEmpty()) - load(builder.buildDrawable()) -} - @SuppressLint("ResourceType") private fun loadContactPictureWithCoil( imageView: ImageView, @@ -508,7 +499,7 @@ private fun getErrorImageLoader( R.drawable.inset_user_circle } } else { - ImageUtils.getGeneratedAvatar(context, size, textSize, initials) + ImageUtils.generatedAvatarIfNeededAndReturnPath(context, size, textSize, initials) } } diff --git a/app/src/main/java/org/linphone/utils/FileUtils.kt b/app/src/main/java/org/linphone/utils/FileUtils.kt index 8500de8e74..79a6e791d1 100644 --- a/app/src/main/java/org/linphone/utils/FileUtils.kt +++ b/app/src/main/java/org/linphone/utils/FileUtils.kt @@ -413,16 +413,20 @@ class FileUtils { } @AnyThread - fun storeBitmap(bitmap: Bitmap, fileName: String): String { - val path = getFileStorageCacheDir("$fileName.jpg", true) - FileOutputStream(path).use { outputStream -> + fun storeBitmap(bitmap: Bitmap, file: File): String { + val format = if (file.absolutePath.endsWith(".png")) { + Bitmap.CompressFormat.PNG + } else { + Bitmap.CompressFormat.JPEG + } + FileOutputStream(file).use { outputStream -> bitmap.compress( - Bitmap.CompressFormat.JPEG, + format, 100, outputStream ) } - return path.absolutePath + return file.absolutePath } @AnyThread diff --git a/app/src/main/java/org/linphone/utils/ImageUtils.kt b/app/src/main/java/org/linphone/utils/ImageUtils.kt index 52144be88a..7016f1943f 100644 --- a/app/src/main/java/org/linphone/utils/ImageUtils.kt +++ b/app/src/main/java/org/linphone/utils/ImageUtils.kt @@ -27,7 +27,6 @@ import android.graphics.Paint import android.graphics.PorterDuff import android.graphics.PorterDuffXfermode import android.graphics.Rect -import android.graphics.drawable.BitmapDrawable import androidx.annotation.AnyThread import androidx.annotation.WorkerThread import java.io.FileNotFoundException @@ -41,7 +40,13 @@ class ImageUtils { private const val TAG = "[Image Utils]" @AnyThread - fun getGeneratedAvatar(context: Context, size: Int = 0, textSize: Int = 0, initials: String): BitmapDrawable { + fun generatedAvatarIfNeededAndReturnPath(context: Context, size: Int = 0, textSize: Int = 0, initials: String): String { + val generatedAvatarPath = FileUtils.getFileStorageCacheDir("$initials.png", overrideExisting = true) + if (generatedAvatarPath.exists()) { + val path = generatedAvatarPath.absolutePath + return path + } + val builder = AvatarGenerator(context) builder.setInitials(initials) if (size > 0) { @@ -52,7 +57,9 @@ class ImageUtils { if (textSize > 0) { builder.setTextSize(AppUtils.getDimension(textSize)) } - return builder.buildDrawable() + val bitmap = builder.buildBitmap(true) + val path = FileUtils.storeBitmap(bitmap, generatedAvatarPath) + return path } @WorkerThread From ab6911dd112b4b5e876d8a38f78d9abd56263a9d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 21 Oct 2025 16:46:07 +0200 Subject: [PATCH 326/593] Fixed adding/editing CardDAV synchronized contact picture --- .../ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt index 02bdf318ac..b78d170040 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt @@ -135,8 +135,9 @@ class ContactNewOrEditViewModel @AnyThread fun getPictureFileName(): String { - val name = id.value?.replace(" ", "_") ?: "${firstName.value.orEmpty().trim()}_${lastName.value.orEmpty().trim()}" - return "$name.jpg" + val name = id.value ?: "${firstName.value.orEmpty().trim()}_${lastName.value.orEmpty().trim()}" + val flattenedName = name.replace(" ", "_").replace(":", "") + return "$flattenedName.jpg" } @UiThread From e267f46fd742e92c0b11a3bf59ea9ee508640400 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 22 Oct 2025 09:29:48 +0200 Subject: [PATCH 327/593] Prevent blinking avatars in list in case they didn't change --- .../ui/main/chat/model/ConversationModel.kt | 22 +++++++++++-------- .../contacts/adapter/ContactsListAdapter.kt | 2 +- .../main/contacts/model/ContactAvatarModel.kt | 22 +++++++++++++++++++ .../history/adapter/HistoryListAdapter.kt | 2 +- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt index 1ad3cb8e6c..60b6d323c0 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt @@ -431,16 +431,20 @@ class ConversationModel } if (isGroup) { - val fakeFriend = coreContext.core.createFriend() - fakeFriend.name = chatRoom.subject - val model = ContactAvatarModel(fakeFriend) - model.defaultToConversationIcon.postValue(true) - model.updateSecurityLevelUsingConversation(chatRoom) - avatarModel.postValue(model) + if (avatarModel.value == null) { + val fakeFriend = coreContext.core.createFriend() + fakeFriend.name = chatRoom.subject + val model = ContactAvatarModel(fakeFriend) + model.defaultToConversationIcon.postValue(true) + model.updateSecurityLevelUsingConversation(chatRoom) + avatarModel.postValue(model) + } } else { - avatarModel.postValue( - coreContext.contactsManager.getContactAvatarModelForAddress(address) - ) + val model = coreContext.contactsManager.getContactAvatarModelForAddress(address) + val oldModel = avatarModel.value + if (!model.compare(oldModel)) { + avatarModel.postValue(model) + } } } diff --git a/app/src/main/java/org/linphone/ui/main/contacts/adapter/ContactsListAdapter.kt b/app/src/main/java/org/linphone/ui/main/contacts/adapter/ContactsListAdapter.kt index b3572b43c2..f27a6ae53a 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/adapter/ContactsListAdapter.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/adapter/ContactsListAdapter.kt @@ -160,7 +160,7 @@ class ContactsListAdapter( } override fun areContentsTheSame(oldItem: ContactAvatarModel, newItem: ContactAvatarModel): Boolean { - return false // oldItem & newItem are always the same because fetched from cache, so return false to force refresh + return newItem.compare(oldItem) } } } diff --git a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt index 065d9a29ae..3282a25d8c 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt @@ -20,6 +20,7 @@ package org.linphone.ui.main.contacts.model import android.net.Uri +import androidx.annotation.AnyThread import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import org.linphone.LinphoneApplication.Companion.coreContext @@ -84,6 +85,27 @@ class ContactAvatarModel refreshSortingName() } + @AnyThread + fun compare(other: ContactAvatarModel?): Boolean { + if (other == null) return false + + val picture = picturePath.value + val otherPicture = other.picturePath.value + if (picture != null && otherPicture != null && picture != otherPicture) { + return false + } + + if (contactName != other.contactName) { + return false + } + + if (id != other.id) { + return false + } + + return true + } + @WorkerThread fun destroy() { if (friend.addresses.isNotEmpty()) { diff --git a/app/src/main/java/org/linphone/ui/main/history/adapter/HistoryListAdapter.kt b/app/src/main/java/org/linphone/ui/main/history/adapter/HistoryListAdapter.kt index 63af2740e7..7b733e0870 100644 --- a/app/src/main/java/org/linphone/ui/main/history/adapter/HistoryListAdapter.kt +++ b/app/src/main/java/org/linphone/ui/main/history/adapter/HistoryListAdapter.kt @@ -107,7 +107,7 @@ class HistoryListAdapter : ListAdapter(Ca } override fun areContentsTheSame(oldItem: CallLogModel, newItem: CallLogModel): Boolean { - return false // ContactAvatarModel will be the same object but with an updated content + return newItem.avatarModel.compare(oldItem.avatarModel) } } } From ce2b794936d4d6beb426c7bbae780d441d773cbc Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 22 Oct 2025 10:02:28 +0200 Subject: [PATCH 328/593] Fixed conversation avatar not updated when subject changes --- .../org/linphone/ui/main/chat/model/ConversationModel.kt | 3 ++- .../ui/main/chat/viewmodel/ConversationInfoViewModel.kt | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt index 60b6d323c0..6e6a56c45a 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt @@ -158,6 +158,7 @@ class ConversationModel override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) { Log.i("$TAG Conversation subject changed [${chatRoom.subject}]") subject.postValue(chatRoom.subject) + computeParticipants() } @WorkerThread @@ -431,7 +432,7 @@ class ConversationModel } if (isGroup) { - if (avatarModel.value == null) { + if (avatarModel.value == null || avatarModel.value?.contactName != chatRoom.subject) { val fakeFriend = coreContext.core.createFriend() fakeFriend.name = chatRoom.subject val model = ContactAvatarModel(fakeFriend) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt index dedac0fd27..4ac6aadd0b 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt @@ -159,6 +159,7 @@ class ConversationInfoViewModel showGreenToast(R.string.conversation_subject_changed_toast, R.drawable.check) subject.postValue(chatRoom.subject) + computeParticipantsList() infoChangedEvent.postValue(Event(true)) } @@ -562,7 +563,9 @@ class ConversationInfoViewModel } else { participantsList.first().avatarModel } - avatarModel.postValue(avatar) + if (!avatar.compare(avatarModel.value)) { + avatarModel.postValue(avatar) + } participants.postValue(participantsList) participantsLabel.postValue( From bdd5c8766b8a9554c5023d5f28c88cc01c898e93 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 27 Oct 2025 10:30:26 +0100 Subject: [PATCH 329/593] Fixed missing bottom margin --- app/src/main/res/layout/settings_developer_fragment.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/settings_developer_fragment.xml b/app/src/main/res/layout/settings_developer_fragment.xml index 1b3b3271af..b585f803d1 100644 --- a/app/src/main/res/layout/settings_developer_fragment.xml +++ b/app/src/main/res/layout/settings_developer_fragment.xml @@ -297,11 +297,14 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="10dp" + android:layout_marginBottom="@dimen/screen_bottom_margin" android:maxLines="3" android:text="@string/settings_developer_clear_native_friends_in_database_subtitle" + app:layout_constraintVertical_bias="0" app:layout_constraintTop_toBottomOf="@id/clear_friends_db_label" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent"/> + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="parent"/> From 3698e1673e2588d6d0f1e5679081357e2b344032 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 27 Oct 2025 11:20:37 +0100 Subject: [PATCH 330/593] Removed delete contact option for native ones, will be re-imported at next restart anyway --- .../ui/main/contacts/fragment/ContactsListFragment.kt | 1 + .../main/contacts/fragment/ContactsListMenuDialogFragment.kt | 2 ++ .../linphone/ui/main/contacts/model/ContactAvatarModel.kt | 2 ++ .../linphone/ui/main/contacts/viewmodel/ContactViewModel.kt | 4 ++++ app/src/main/res/layout/contact_fragment.xml | 4 ++-- app/src/main/res/layout/contacts_list_long_press_menu.xml | 5 ++++- 6 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt index 02df724a71..0457543fa7 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt @@ -282,6 +282,7 @@ class ContactsListFragment : AbstractMainFragment() { model.isFavourite.value == true, model.isStored, isReadOnly = model.isReadOnly, + isNative = model.isNative, { // onDismiss adapter.resetSelection() }, diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListMenuDialogFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListMenuDialogFragment.kt index 497dbc9ca6..8824d6c3d4 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListMenuDialogFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListMenuDialogFragment.kt @@ -36,6 +36,7 @@ class ContactsListMenuDialogFragment( private val isFavourite: Boolean, private val isStored: Boolean, private val isReadOnly: Boolean, + private val isNative: Boolean, private val onDismiss: (() -> Unit)? = null, private val onFavourite: (() -> Unit)? = null, private val onShare: (() -> Unit)? = null, @@ -72,6 +73,7 @@ class ContactsListMenuDialogFragment( view.isFavourite = isFavourite view.isStored = isStored view.isReadOnly = isReadOnly + view.isNative = isNative view.setFavoriteClickListener { onFavourite?.invoke() diff --git a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt index 3282a25d8c..eb8d5b55ef 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt @@ -54,6 +54,8 @@ class ContactAvatarModel val isReadOnly = friend.isReadOnly + val isNative = !friend.nativeUri.isNullOrEmpty() + val isFavourite = MutableLiveData() val lastPresenceInfo = MutableLiveData() diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt index ac2ea42543..96f8b99545 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt @@ -93,6 +93,8 @@ class ContactViewModel val isReadOnly = MutableLiveData() + val isNative = MutableLiveData() + val chatDisabled = MutableLiveData() val videoCallDisabled = MutableLiveData() @@ -236,6 +238,7 @@ class ContactViewModel init { isStored.value = false isReadOnly.value = false + isNative.value = false expandNumbersAndAddresses.value = true trustedDevicesPercentage.value = 0 @@ -312,6 +315,7 @@ class ContactViewModel // if they are in a temporary one (for example if they are from a remote directory such as LDAP or CardDAV) isStored.postValue(!coreContext.contactsManager.isContactTemporary(friend)) isReadOnly.postValue(friend.isReadOnly) + isNative.postValue(!friend.nativeUri.isNullOrEmpty()) contact.value?.destroy() contact.postValue(ContactAvatarModel(friend)) diff --git a/app/src/main/res/layout/contact_fragment.xml b/app/src/main/res/layout/contact_fragment.xml index 145ed224a5..4d37caf003 100644 --- a/app/src/main/res/layout/contact_fragment.xml +++ b/app/src/main/res/layout/contact_fragment.xml @@ -561,7 +561,7 @@ android:background="@drawable/action_background_bottom" android:text="@string/contact_details_delete" android:drawableStart="@drawable/trash_simple" - android:visibility="@{viewModel.isStored && !viewModel.isReadOnly ? View.VISIBLE : View.GONE}" + android:visibility="@{viewModel.isStored && !viewModel.isReadOnly && !viewModel.isNative ? View.VISIBLE : View.GONE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/action_share"/> @@ -573,7 +573,7 @@ android:layout_marginEnd="16dp" android:background="?attr/color_separator" android:importantForAccessibility="no" - android:visibility="@{viewModel.isStored && !viewModel.isReadOnly ? View.VISIBLE : View.GONE}" + android:visibility="@{viewModel.isStored && !viewModel.isReadOnly && !viewModel.isNative ? View.VISIBLE : View.GONE}" app:layout_constraintEnd_toEndOf="@id/action_edit" app:layout_constraintStart_toStartOf="@id/action_edit" app:layout_constraintTop_toBottomOf="@+id/action_edit"/> diff --git a/app/src/main/res/layout/contacts_list_long_press_menu.xml b/app/src/main/res/layout/contacts_list_long_press_menu.xml index 152a60981d..921fbf323c 100644 --- a/app/src/main/res/layout/contacts_list_long_press_menu.xml +++ b/app/src/main/res/layout/contacts_list_long_press_menu.xml @@ -25,6 +25,9 @@ + Date: Mon, 27 Oct 2025 14:13:32 +0100 Subject: [PATCH 331/593] Fixed contacts list cell clipping --- .../ui/main/contacts/fragment/ContactsListFragment.kt | 5 +++++ app/src/main/res/layout/contact_list_cell.xml | 4 ++-- app/src/main/res/layout/contacts_list_fragment.xml | 8 ++++---- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt index 0457543fa7..0cea2cdf15 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt @@ -136,6 +136,7 @@ class ContactsListFragment : AbstractMainFragment() { binding.contactsList.setHasFixedSize(true) binding.contactsList.layoutManager = LinearLayoutManager(requireContext()) + binding.contactsList.outlineProvider = outlineProvider binding.favouritesContactsList.setHasFixedSize(true) val favouritesLayoutManager = LinearLayoutManager(requireContext()) @@ -145,6 +146,10 @@ class ContactsListFragment : AbstractMainFragment() { configureAdapter(adapter) configureAdapter(favouritesAdapter) + listViewModel.isListFiltered.observe(viewLifecycleOwner) { filtered -> + binding.contactsList.clipToOutline = filtered + } + listViewModel.contactsList.observe( viewLifecycleOwner ) { diff --git a/app/src/main/res/layout/contact_list_cell.xml b/app/src/main/res/layout/contact_list_cell.xml index e747776fef..89031a693c 100644 --- a/app/src/main/res/layout/contact_list_cell.xml +++ b/app/src/main/res/layout/contact_list_cell.xml @@ -25,8 +25,8 @@ android:onContextClick="@{onLongClickListener}" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="4dp" - android:layout_marginEnd="16dp" + android:paddingStart="4dp" + android:paddingEnd="16dp" android:paddingTop="5dp" android:paddingBottom="5dp" android:background="@drawable/primary_cell_background"> diff --git a/app/src/main/res/layout/contacts_list_fragment.xml b/app/src/main/res/layout/contacts_list_fragment.xml index 9d4717c016..201628e4c8 100644 --- a/app/src/main/res/layout/contacts_list_fragment.xml +++ b/app/src/main/res/layout/contacts_list_fragment.xml @@ -81,6 +81,7 @@ android:layout_marginStart="16dp" android:layout_marginEnd="16dp" android:layout_marginTop="10dp" + android:layout_marginBottom="4dp" android:text="@string/contacts_list_favourites_title" android:drawableEnd="@{viewModel.showFavourites ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" /> @@ -89,8 +90,7 @@ android:id="@+id/favourites_contacts_list" android:visibility="@{viewModel.showFavourites && !viewModel.isListFiltered && viewModel.favouritesList.size() > 0 ? View.VISIBLE : View.GONE}" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="4dp" /> + android:layout_height="wrap_content" /> + android:layout_height="match_parent"> Date: Mon, 27 Oct 2025 15:17:18 +0100 Subject: [PATCH 332/593] Fixed generated avatars color when switching between light/dark modes --- .../main/java/org/linphone/contacts/AvatarGenerator.kt | 6 ------ app/src/main/java/org/linphone/utils/ImageUtils.kt | 8 ++++++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/linphone/contacts/AvatarGenerator.kt b/app/src/main/java/org/linphone/contacts/AvatarGenerator.kt index 704e5543d3..a66cda5123 100644 --- a/app/src/main/java/org/linphone/contacts/AvatarGenerator.kt +++ b/app/src/main/java/org/linphone/contacts/AvatarGenerator.kt @@ -25,7 +25,6 @@ import android.graphics.Canvas import android.graphics.Paint import android.graphics.Rect import android.graphics.RectF -import android.graphics.drawable.BitmapDrawable import android.text.TextPaint import android.util.TypedValue import androidx.core.content.ContextCompat @@ -34,7 +33,6 @@ import androidx.core.graphics.drawable.IconCompat import org.linphone.R import org.linphone.utils.AppUtils import androidx.core.graphics.createBitmap -import androidx.core.graphics.drawable.toDrawable class AvatarGenerator(private val context: Context) { private var textSize: Float = AppUtils.getDimension(R.dimen.avatar_initials_text_size) @@ -92,10 +90,6 @@ class AvatarGenerator(private val context: Context) { return bitmap } - fun buildDrawable(): BitmapDrawable { - return buildBitmap(true).toDrawable(context.resources) - } - fun buildIcon(): IconCompat { return IconCompat.createWithAdaptiveBitmap(buildBitmap(false)) } diff --git a/app/src/main/java/org/linphone/utils/ImageUtils.kt b/app/src/main/java/org/linphone/utils/ImageUtils.kt index 7016f1943f..1367b8cd0c 100644 --- a/app/src/main/java/org/linphone/utils/ImageUtils.kt +++ b/app/src/main/java/org/linphone/utils/ImageUtils.kt @@ -20,6 +20,7 @@ package org.linphone.utils import android.content.Context +import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.ImageDecoder @@ -41,7 +42,10 @@ class ImageUtils { @AnyThread fun generatedAvatarIfNeededAndReturnPath(context: Context, size: Int = 0, textSize: Int = 0, initials: String): String { - val generatedAvatarPath = FileUtils.getFileStorageCacheDir("$initials.png", overrideExisting = true) + val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + val suffix = if (darkMode) "_dark" else "_light" + + val generatedAvatarPath = FileUtils.getFileStorageCacheDir("$initials$suffix.png", overrideExisting = true) if (generatedAvatarPath.exists()) { val path = generatedAvatarPath.absolutePath return path @@ -57,7 +61,7 @@ class ImageUtils { if (textSize > 0) { builder.setTextSize(AppUtils.getDimension(textSize)) } - val bitmap = builder.buildBitmap(true) + val bitmap = builder.buildBitmap(false) val path = FileUtils.storeBitmap(bitmap, generatedAvatarPath) return path } From c3ad96cd1f1cec3275e64bb4b709e2607b78a3c5 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 27 Oct 2025 15:21:48 +0100 Subject: [PATCH 333/593] Fixed issue in contact layout --- app/src/main/res/layout/contact_fragment.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/contact_fragment.xml b/app/src/main/res/layout/contact_fragment.xml index 4d37caf003..3915c4d147 100644 --- a/app/src/main/res/layout/contact_fragment.xml +++ b/app/src/main/res/layout/contact_fragment.xml @@ -543,7 +543,7 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:background="@{viewModel.isStored && !viewModel.isReadOnly ? @drawable/action_background_middle : @drawable/action_background_full, default=@drawable/action_background_middle}" + android:background="@{viewModel.isStored && !viewModel.isReadOnly ? (viewModel.isNative ? @drawable/action_background_bottom : @drawable/action_background_middle) : @drawable/action_background_full, default=@drawable/action_background_middle}" android:text="@string/contact_details_share" android:drawableStart="@drawable/share_network" app:layout_constraintStart_toStartOf="parent" @@ -573,7 +573,7 @@ android:layout_marginEnd="16dp" android:background="?attr/color_separator" android:importantForAccessibility="no" - android:visibility="@{viewModel.isStored && !viewModel.isReadOnly && !viewModel.isNative ? View.VISIBLE : View.GONE}" + android:visibility="@{viewModel.isStored && !viewModel.isReadOnly ? View.VISIBLE : View.GONE}" app:layout_constraintEnd_toEndOf="@id/action_edit" app:layout_constraintStart_toStartOf="@id/action_edit" app:layout_constraintTop_toBottomOf="@+id/action_edit"/> @@ -597,7 +597,7 @@ android:layout_marginEnd="16dp" android:background="?attr/color_separator" android:importantForAccessibility="no" - android:visibility="@{viewModel.isStored && !viewModel.isReadOnly ? View.VISIBLE : View.GONE}" + android:visibility="@{viewModel.isStored && !viewModel.isReadOnly && !viewModel.isNative() ? View.VISIBLE : View.GONE}" app:layout_constraintEnd_toEndOf="@id/action_share" app:layout_constraintStart_toStartOf="@id/action_share" app:layout_constraintTop_toBottomOf="@+id/action_share"/> From e14ea0ac686defba4d9a0d46b92404fdaf44539d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 29 Oct 2025 10:50:50 +0100 Subject: [PATCH 334/593] Fixed emoji reaction not visible when long pressing an image on a device with a small screen --- .../main/res/layout/chat_bubble_long_press_menu.xml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/layout/chat_bubble_long_press_menu.xml b/app/src/main/res/layout/chat_bubble_long_press_menu.xml index 01bcd9bcbf..2fb5f209d1 100644 --- a/app/src/main/res/layout/chat_bubble_long_press_menu.xml +++ b/app/src/main/res/layout/chat_bubble_long_press_menu.xml @@ -25,7 +25,7 @@ android:id="@+id/emojis" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginBottom="5dp" + android:layout_marginTop="10dp" android:layout_marginStart="10dp" android:layout_marginEnd="10dp" android:visibility="@{viewModel.isChatRoomReadOnly ? View.GONE : View.VISIBLE}" @@ -33,7 +33,10 @@ bind:model="@{viewModel.messageModel}" bind:pickEmojiClickListener="@{() -> viewModel.pickEmoji()}" app:layout_constraintHorizontal_bias="@{viewModel.horizontalBias, default=0}" + app:layout_constraintVertical_bias="1" app:layout_constraintWidth_max="@dimen/emoji_list_max_width" + app:layout_constraintVertical_chainStyle="packed" + app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@id/bubbles" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> @@ -45,8 +48,10 @@ android:layout_marginStart="10dp" android:layout_marginEnd="10dp" android:layout_marginBottom="5dp" + app:layout_constrainedHeight="true" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/emojis" app:layout_constraintBottom_toTopOf="@id/actions"> + app:layout_constraintEnd_toEndOf="parent"/> + app:layout_constraintEnd_toEndOf="parent"/> From daa2f10f7bf84afd1afa50e00f48e6167526fc98 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 30 Oct 2025 12:39:43 +0100 Subject: [PATCH 335/593] Bumped Kotlin version --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 52647c3646..a360709e62 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] agp = "8.13.0" -kotlin = "2.2.20" +kotlin = "2.2.21" gmsGoogleServices = "4.4.4" firebaseCrashlytics = "3.0.6" firebaseBomVersion = "34.4.0" From 28cee7f53999203823730acdac2b91d4cc6352e2 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 31 Oct 2025 09:44:20 +0100 Subject: [PATCH 336/593] Hide LDAP verbose mode toggle setting as it has no effect in SDK --- app/src/main/res/layout/settings_contacts_ldap.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/settings_contacts_ldap.xml b/app/src/main/res/layout/settings_contacts_ldap.xml index 5fcad6d624..f388fc8805 100644 --- a/app/src/main/res/layout/settings_contacts_ldap.xml +++ b/app/src/main/res/layout/settings_contacts_ldap.xml @@ -255,7 +255,8 @@ android:text="@string/settings_contacts_ldap_verbose_mode_title" android:maxLines="2" android:ellipsize="end" - android:labelFor="@id/tls_switch" + android:labelFor="@id/debug_switch" + android:visibility="gone" app:layout_constraintTop_toTopOf="@id/debug_switch" app:layout_constraintBottom_toBottomOf="@id/debug_switch" app:layout_constraintStart_toStartOf="parent" @@ -270,6 +271,7 @@ android:layout_marginTop="20dp" android:layout_marginEnd="16dp" android:checked="@{viewModel.verboseMode}" + android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/tls_switch" /> From a3f86fbac0520d26c8b02d1dec36e9be0d35978b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 31 Oct 2025 14:37:00 +0100 Subject: [PATCH 337/593] Fixed toggling favorite flag on contact not adding/removing it from favorites list --- .../org/linphone/ui/main/contacts/model/ContactAvatarModel.kt | 4 ++++ .../ui/main/contacts/viewmodel/ContactsListViewModel.kt | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt index eb8d5b55ef..240e2f3c35 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt @@ -105,6 +105,10 @@ class ContactAvatarModel return false } + if (isFavourite.value != other.isFavourite.value) { + return false + } + return true } diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt index f931488500..1eb63eb9f8 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt @@ -137,7 +137,8 @@ class ContactsListViewModel applyFilter( currentFilter, - domainFilter + domainFilter, + true ) } From 89458ed82634ae2d40c2999e9a90563d8bb80413 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 3 Nov 2025 10:18:34 +0100 Subject: [PATCH 338/593] Bumped firebase BoM --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a360709e62..41edf105b1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ agp = "8.13.0" kotlin = "2.2.21" gmsGoogleServices = "4.4.4" firebaseCrashlytics = "3.0.6" -firebaseBomVersion = "34.4.0" +firebaseBomVersion = "34.5.0" #noinspection NewerVersionAvailable ktlint = "12.3.0" From 7b0de4185c0e07a90b96ab5d6fd3c93dbbd965ab Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 5 Nov 2025 14:47:13 +0100 Subject: [PATCH 339/593] Prevent message edit to overlap reply and vice-versa --- .../chat/viewmodel/SendMessageInConversationViewModel.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt index 690c47b707..0e9cac65c0 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt @@ -236,6 +236,10 @@ class SendMessageInConversationViewModel @UiThread fun editMessage(model: MessageModel) { + if (isReplying.value == true) { + cancelReply() + } + val newValue = model.text.value?.toString() ?: "" textToSend.value = newValue @@ -258,6 +262,10 @@ class SendMessageInConversationViewModel @UiThread fun replyToMessage(model: MessageModel) { + if (isEditing.value == true) { + cancelEdit() + } + coreContext.postOnCoreThread { val message = model.chatMessage Log.i("$TAG Pending reply to message [${message.messageId}]") From 209c0df09199c0ed03aad1fd80923e28e17c2cc6 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 6 Nov 2025 08:50:39 +0100 Subject: [PATCH 340/593] Prevent voice message recording when editing an already sent message --- app/src/main/res/layout/chat_conversation_send_area.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/chat_conversation_send_area.xml b/app/src/main/res/layout/chat_conversation_send_area.xml index 24a6c28bfe..35c28d99aa 100644 --- a/app/src/main/res/layout/chat_conversation_send_area.xml +++ b/app/src/main/res/layout/chat_conversation_send_area.xml @@ -173,7 +173,7 @@ android:layout_height="0dp" android:layout_marginEnd="4dp" android:enabled="@{viewModel.textToSend.length() > 0 || viewModel.attachments.size() > 0}" - android:visibility="@{viewModel.isCallConversation || viewModel.textToSend.length() > 0 || viewModel.attachments.size() > 0 ? View.VISIBLE : View.GONE, default=gone}" + android:visibility="@{viewModel.isCallConversation || viewModel.textToSend.length() > 0 || viewModel.attachments.size() > 0 || viewModel.isEditing ? View.VISIBLE : View.GONE, default=gone}" android:onClick="@{() -> viewModel.sendMessage()}" android:padding="8dp" android:src="@{viewModel.isEditing ? @drawable/pencil_simple : @drawable/paper_plane_right, default=@drawable/paper_plane_right}" @@ -188,7 +188,7 @@ android:layout_width="40dp" android:layout_height="0dp" android:layout_marginEnd="4dp" - android:visibility="@{viewModel.isCallConversation || viewModel.textToSend.length() > 0 || viewModel.attachments.size() > 0 || viewModel.isVoiceRecording || !viewModel.isFileTransferServerAvailable ? View.GONE : View.VISIBLE}" + android:visibility="@{viewModel.isCallConversation || viewModel.textToSend.length() > 0 || viewModel.attachments.size() > 0 || viewModel.isVoiceRecording || !viewModel.isFileTransferServerAvailable || viewModel.isEditing ? View.GONE : View.VISIBLE}" android:onClick="@{() -> viewModel.startVoiceMessageRecording()}" android:padding="8dp" android:src="@drawable/microphone" From a6f568497d7ef2455e41d4e54cebf8598d35d5fc Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 6 Nov 2025 09:04:43 +0100 Subject: [PATCH 341/593] Bumped dependencies --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 41edf105b1..215c2768dc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ activity = "1.11.0" appcompat = "1.7.1" constraintLayout = "2.2.1" coreKtx = "1.17.0" -splashscreen = "1.2.0-rc01" +splashscreen = "1.2.0" telecom = "1.0.1" media = "1.7.1" recyclerview = "1.4.0" @@ -21,7 +21,7 @@ slidingpanelayout = "1.2.0" window = "1.5.0" gridlayout = "1.1.0" securityCryptoKtx = "1.1.0" -navigation = "2.9.5" +navigation = "2.9.6" emoji2 = "1.6.0" car = "1.7.0" flexbox = "3.0.0" From 9afcb6db15cc86f734343a8441838ad78bbf09ee Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 17 Nov 2025 09:47:56 +0100 Subject: [PATCH 342/593] Updated AGP to 8.13.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 215c2768dc..8e69563cc1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.13.0" +agp = "8.13.1" kotlin = "2.2.21" gmsGoogleServices = "4.4.4" firebaseCrashlytics = "3.0.6" From d5c836b8b5f4d6f63f63d13720f0684a4e5911b2 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 17 Nov 2025 10:06:01 +0100 Subject: [PATCH 343/593] Should prevent crash that may happen after picking ringtone if settings fragment was destroyed while new ringtone was being picked --- .../settings/fragment/SettingsFragment.kt | 19 ++++--------------- .../layout/settings_developer_fragment.xml | 3 +-- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt index fa915e656f..90c3fbb65b 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt @@ -30,8 +30,8 @@ import android.view.ViewGroup import android.widget.AdapterView import android.widget.ArrayAdapter import androidx.annotation.UiThread -import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels import org.linphone.R import org.linphone.compatibility.Compatibility import org.linphone.core.tools.Log @@ -54,7 +54,9 @@ class SettingsFragment : GenericMainFragment() { private lateinit var binding: SettingsFragmentBinding - private lateinit var viewModel: SettingsViewModel + private val viewModel: SettingsViewModel by navGraphViewModels( + R.id.main_nav_graph + ) private val sortContactsByListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { @@ -138,8 +140,6 @@ class SettingsFragment : GenericMainFragment() { postponeEnterTransition() super.onViewCreated(view, savedInstanceState) - viewModel = ViewModelProvider(this)[SettingsViewModel::class.java] - binding.lifecycleOwner = viewLifecycleOwner binding.viewModel = viewModel observeToastEvents(viewModel) @@ -179,17 +179,6 @@ class SettingsFragment : GenericMainFragment() { viewModel.goToIncomingCallNotificationChannelSettingsEvent.observe(viewLifecycleOwner) { it.consume { currentRingtone -> try { - /* - Log.w("$TAG Going to incoming call channel settings") - val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { - putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) - putExtra( - Settings.EXTRA_CHANNEL_ID, - getString(R.string.notification_channel_without_ringtone_incoming_call_id) - ) - } - startActivity(intent) - */ val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { putExtra( RingtoneManager.EXTRA_RINGTONE_TYPE, diff --git a/app/src/main/res/layout/settings_developer_fragment.xml b/app/src/main/res/layout/settings_developer_fragment.xml index b585f803d1..65b47a64ff 100644 --- a/app/src/main/res/layout/settings_developer_fragment.xml +++ b/app/src/main/res/layout/settings_developer_fragment.xml @@ -274,7 +274,7 @@ style="@style/settings_title_style" android:id="@+id/clear_friends_db_label" android:onClick="@{() -> viewModel.clearNativeFriendsDatabase()}" - android:layout_width="0dp" + android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="10dp" @@ -286,7 +286,6 @@ android:drawableEnd="@drawable/warning_circle" android:drawableTint="?attr/color_main2_600" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/push_compatible_domains_list"/> Date: Fri, 7 Nov 2025 16:09:30 +0100 Subject: [PATCH 344/593] Ignore Telecom Manager endpoints availability/requests, using our own preferred endpoint policy (to workaround device disconnect/reconnect not always notified) --- .../java/org/linphone/core/CoreContext.kt | 55 ++++-- .../java/org/linphone/core/CorePreferences.kt | 7 + .../telecom/TelecomCallControlCallback.kt | 163 ++---------------- .../org/linphone/telecom/TelecomManager.kt | 20 --- .../java/org/linphone/utils/AudioUtils.kt | 25 +-- 5 files changed, 63 insertions(+), 207 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 487e9b42e4..f80055ac41 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -52,6 +52,7 @@ import org.linphone.telecom.TelecomManager import org.linphone.ui.call.CallActivity import org.linphone.utils.ActivityMonitor import org.linphone.utils.AppUtils +import org.linphone.utils.AudioUtils import org.linphone.utils.Event import org.linphone.utils.FileUtils import org.linphone.utils.LinphoneUtils @@ -143,20 +144,29 @@ class CoreContext override fun onAudioDevicesAdded(addedDevices: Array?) { if (!addedDevices.isNullOrEmpty()) { Log.i("$TAG [${addedDevices.size}] new device(s) have been added:") + var atLeastOneNewDeviceIsBluetooth = false for (device in addedDevices) { Log.i( "$TAG Added device [${device.productName}] with ID [${device.id}] and type [${device.type}]" ) - } - if (telecomManager.getCurrentlyFollowedCalls() <= 0) { - Log.i("$TAG No call found in Telecom's CallsManager, reloading sound devices in 500ms") - postOnCoreThreadDelayed({ core.reloadSoundDevices() }, 500) - } else { - Log.i( - "$TAG At least one active call in Telecom's CallsManager, let it handle the added device(s)" - ) + when (device.type) { + AudioDeviceInfo.TYPE_BLUETOOTH_SCO, AudioDeviceInfo.TYPE_BLE_HEADSET, AudioDeviceInfo.TYPE_BLE_SPEAKER, AudioDeviceInfo.TYPE_HEARING_AID -> { + atLeastOneNewDeviceIsBluetooth = true + } + } } + + Log.i("$TAG Reloading sound devices in 500ms") + postOnCoreThreadDelayed({ + Log.i("$TAG Reloading sound devices") + core.reloadSoundDevices() + + if (atLeastOneNewDeviceIsBluetooth && core.callsNb > 0 && corePreferences.routeAudioToBluetoothWhenPossible) { + Log.i("$TAG It seems a bluetooth device is now available, trying to route audio to it") + AudioUtils.routeAudioToEitherBluetoothOrHearingAid() + } + }, 500) } } @@ -169,14 +179,12 @@ class CoreContext "$TAG Removed device [${device.id}][${device.productName}][${device.type}]" ) } - if (telecomManager.getCurrentlyFollowedCalls() <= 0) { - Log.i("$TAG No call found in Telecom's CallsManager, reloading sound devices in 500ms") - postOnCoreThreadDelayed({ core.reloadSoundDevices() }, 500) - } else { - Log.i( - "$TAG At least one active call in Telecom's CallsManager, let it handle the removed device(s)" - ) - } + + Log.i("$TAG Reloading sound devices in 500ms") + postOnCoreThreadDelayed({ + Log.i("$TAG Reloading sound devices") + core.reloadSoundDevices() + }, 500) } } } @@ -348,10 +356,20 @@ class CoreContext ) } } + Call.State.OutgoingRinging, Call.State.OutgoingEarlyMedia -> { + if (corePreferences.routeAudioToBluetoothWhenPossible) { + Log.i("$TAG Trying to route audio to either bluetooth or hearing aid if available") + AudioUtils.routeAudioToEitherBluetoothOrHearingAid(call) + } + } Call.State.Connected -> { postOnMainThread { showCallActivity() } + if (corePreferences.routeAudioToBluetoothWhenPossible) { + Log.i("$TAG Call is connected, trying to route audio to either bluetooth or hearing aid if available") + AudioUtils.routeAudioToEitherBluetoothOrHearingAid(call) + } } Call.State.StreamsRunning -> { if (previousCallState == Call.State.Connected) { @@ -407,6 +425,11 @@ class CoreContext Log.i("$TAG Available audio devices list was updated") } + @WorkerThread + override fun onFirstCallStarted(core: Core) { + Log.i("$TAG First call started") + } + @WorkerThread override fun onLastCallEnded(core: Core) { Log.i("$TAG Last call ended") diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 953931de7b..51437bbbe4 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -127,6 +127,13 @@ class CorePreferences // Call settings // This won't be done if bluetooth or wired headset is used + @get:AnyThread @set:WorkerThread + var routeAudioToBluetoothWhenPossible: Boolean + get() = config.getBool("app", "route_audio_to_bluetooth_when_possible", true) + set(value) { + config.setBool("app", "route_audio_to_bluetooth_when_possible", value) + } + @get:AnyThread @set:WorkerThread var routeAudioToSpeakerWhenVideoIsEnabled: Boolean get() = config.getBool("app", "route_audio_to_speaker_when_video_enabled", true) diff --git a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt index 46b31ff90a..e9953930d4 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt @@ -26,13 +26,11 @@ import androidx.core.telecom.CallControlResult import androidx.core.telecom.CallControlScope import androidx.core.telecom.CallEndpointCompat import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.core.AudioDevice import org.linphone.core.Call import org.linphone.core.CallListenerStub import org.linphone.core.Reason @@ -125,75 +123,11 @@ class TelecomCallControlCallback( } callControl.availableEndpoints.onEach { list -> - Log.i("$TAG New available audio endpoints list") - if (availableEndpoints != list) { - Log.i( - "$TAG List size of available audio endpoints has changed, reload sound devices in SDK in [$DELAY_BEFORE_RELOADING_SOUND_DEVICES_MS] ms" - ) - coreContext.postOnCoreThreadDelayed({ core -> - core.reloadSoundDevices() - Log.i("$TAG Sound devices reloaded") - }, DELAY_BEFORE_RELOADING_SOUND_DEVICES_MS) - } - - availableEndpoints = list - for (endpoint in list) { - Log.i("$TAG Available audio endpoint [${endpoint.name}]") - } + Log.i("$TAG New available audio endpoints list but ignoring it") }.launchIn(scope) callControl.currentCallEndpoint.onEach { endpoint -> - var newEndpointToUse = endpoint - if (endpointUpdateRequestFromLinphone) { - Log.i("$TAG Linphone requests to use [${endpoint.name}] audio endpoint with type [${endpointTypeToString(endpoint.type)}]") - } else { - Log.i("$TAG Android requests us to use [${endpoint.name}] audio endpoint with type [${endpointTypeToString(endpoint.type)}]") - } - - val requestedEndpoint = latestLinphoneRequestedEndpoint - if (endpointUpdateRequestFromLinphone && requestedEndpoint != null && requestedEndpoint != endpoint) { - Log.w("$TAG WARNING: Linphone requested endpoint [${requestedEndpoint.name}] but Telecom Manager notified endpoint [${endpoint.name}], trying to use the one we requested anyway") - newEndpointToUse = requestedEndpoint - } - - val type = newEndpointToUse.type - currentEndpoint = type - if (!endpointUpdateRequestFromLinphone && !coreContext.isConnectedToAndroidAuto && (type == CallEndpointCompat.Companion.TYPE_EARPIECE || type == CallEndpointCompat.Companion.TYPE_SPEAKER)) { - endpointUpdateRequestFromLinphone = false - Log.w("$TAG Device isn't connected to Android Auto, do not follow system request to change audio endpoint to [${newEndpointToUse.name}] with type [${endpointTypeToString(type)}]") - return@onEach - } - endpointUpdateRequestFromLinphone = false - - // Change audio route in SDK, this way the usual listener will trigger - // and we'll be able to update the UI accordingly - val route = arrayListOf() - when (type) { - CallEndpointCompat.Companion.TYPE_EARPIECE -> { - route.add(AudioDevice.Type.Earpiece) - } - CallEndpointCompat.Companion.TYPE_SPEAKER -> { - route.add(AudioDevice.Type.Speaker) - } - CallEndpointCompat.Companion.TYPE_BLUETOOTH -> { - route.add(AudioDevice.Type.Bluetooth) - route.add(AudioDevice.Type.HearingAid) - } - CallEndpointCompat.Companion.TYPE_WIRED_HEADSET -> { - route.add(AudioDevice.Type.Headphones) - route.add(AudioDevice.Type.Headset) - } - } - if (route.isNotEmpty()) { - coreContext.postOnCoreThread { - if (!AudioUtils.applyAudioRouteChangeInLinphone(call, route)) { - Log.w("$TAG Failed to apply audio route change, trying again in 200ms") - coreContext.postOnCoreThreadDelayed({ - AudioUtils.applyAudioRouteChangeInLinphone(call, route) - }, 200) - } - } - } + Log.i("$TAG Android requests us to use [${endpoint.name}] audio endpoint with type [${endpointTypeToString(endpoint.type)}], ignoring it") }.launchIn(scope) callControl.isMuted.onEach { muted -> @@ -224,89 +158,12 @@ class TelecomCallControlCallback( }.launchIn(scope) } - fun applyAudioRouteToCallWithId(routes: List): Boolean { - Log.i("$TAG Looking for audio endpoint with type [${routes.first()}]") - - var wiredHeadsetFound = false - var skippedBecauseAlreadyInUse = false - for (endpoint in availableEndpoints) { - Log.i( - "$TAG Found audio endpoint [${endpoint.name}] with type [${endpointTypeToString(endpoint.type)}]" - ) - val matches = when (endpoint.type) { - CallEndpointCompat.Companion.TYPE_EARPIECE -> { - routes.find { it == AudioDevice.Type.Earpiece } - } - CallEndpointCompat.Companion.TYPE_SPEAKER -> { - routes.find { it == AudioDevice.Type.Speaker } - } - CallEndpointCompat.Companion.TYPE_BLUETOOTH -> { - routes.find { it == AudioDevice.Type.Bluetooth || it == AudioDevice.Type.HearingAid } - } - CallEndpointCompat.Companion.TYPE_WIRED_HEADSET -> { - wiredHeadsetFound = true - routes.find { it == AudioDevice.Type.Headset || it == AudioDevice.Type.Headphones } - } - else -> null - } - - if (matches != null) { - Log.i( - "$TAG Found matching audio endpoint [${endpoint.name}] with type [${endpointTypeToString(endpoint.type)}], trying to use it" - ) - if (currentEndpoint == endpoint.type) { - Log.w("$TAG Endpoint already in use, skipping") - skippedBecauseAlreadyInUse = true - continue - } - - var success = false - scope.launch { - Log.i("$TAG Requesting audio endpoint change to [${endpoint.name}] with type [${endpointTypeToString(endpoint.type)}]") - endpointUpdateRequestFromLinphone = true - latestLinphoneRequestedEndpoint = endpoint - var result: CallControlResult = callControl.requestEndpointChange(endpoint) - var attempts = 1 - while (result is CallControlResult.Error && attempts <= 2) { - delay(100) - Log.i( - "$TAG Previous attempt failed [$result], requesting again audio endpoint change to [${endpoint.name}] with type [${endpointTypeToString(endpoint.type)}]" - ) - result = callControl.requestEndpointChange(endpoint) - attempts += 1 - } - - if (result is CallControlResult.Error) { - Log.e("$TAG Failed to change endpoint audio device, error [$result]") - } else { - Log.i( - "$TAG It took [$attempts] attempt(s) to change endpoint audio device..." - ) - currentEndpoint = endpoint.type - success = true - } - } - - return success - } - } - - if (routes.size == 1 && routes[0] == AudioDevice.Type.Earpiece && wiredHeadsetFound) { - Log.e("$TAG User asked for earpiece but endpoint doesn't exists!") - } else if (skippedBecauseAlreadyInUse) { - Log.w("$TAG This endpoint was already in use (according to Telecom Manager), force changing the device in Linphone just in case") - } else { - Log.e("$TAG No matching endpoint found") - } - return false - } - private fun answerCall() { val isVideo = LinphoneUtils.isVideoEnabled(call) val type = if (isVideo) { - CallAttributesCompat.Companion.CALL_TYPE_VIDEO_CALL + CallAttributesCompat.CALL_TYPE_VIDEO_CALL } else { - CallAttributesCompat.Companion.CALL_TYPE_AUDIO_CALL + CallAttributesCompat.CALL_TYPE_AUDIO_CALL } scope.launch { Log.i("$TAG Answering [${if (isVideo) "video" else "audio"}] call") @@ -389,12 +246,12 @@ class TelecomCallControlCallback( private fun endpointTypeToString(type: Int): String { return when (type) { - CallEndpointCompat.Companion.TYPE_UNKNOWN -> "UNKNOWN" - CallEndpointCompat.Companion.TYPE_EARPIECE -> "EARPIECE" - CallEndpointCompat.Companion.TYPE_BLUETOOTH -> "BLUETOOTH" - CallEndpointCompat.Companion.TYPE_WIRED_HEADSET -> "WIRED HEADSET" - CallEndpointCompat.Companion.TYPE_SPEAKER -> "SPEAKER" - CallEndpointCompat.Companion.TYPE_STREAMING -> "STREAMING" + CallEndpointCompat.TYPE_UNKNOWN -> "UNKNOWN" + CallEndpointCompat.TYPE_EARPIECE -> "EARPIECE" + CallEndpointCompat.TYPE_BLUETOOTH -> "BLUETOOTH" + CallEndpointCompat.TYPE_WIRED_HEADSET -> "WIRED HEADSET" + CallEndpointCompat.TYPE_SPEAKER -> "SPEAKER" + CallEndpointCompat.TYPE_STREAMING -> "STREAMING" else -> "UNEXPECTED: $type" } } diff --git a/app/src/main/java/org/linphone/telecom/TelecomManager.kt b/app/src/main/java/org/linphone/telecom/TelecomManager.kt index f71b43fe83..4a9867f926 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomManager.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomManager.kt @@ -29,7 +29,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch 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 @@ -89,11 +88,6 @@ class TelecomManager } } - @WorkerThread - fun getCurrentlyFollowedCalls(): Int { - return currentlyFollowedCalls - } - @WorkerThread fun onCallCreated(call: Call) { Log.i("$TAG Call to [${call.remoteAddress.asStringUriOnly()}] created in state [${call.state}]") @@ -208,18 +202,4 @@ class TelecomManager Log.i("$TAG Core is being stopped") core.removeListener(coreListener) } - - @WorkerThread - fun applyAudioRouteToCallWithId(routes: List, callId: String): Boolean { - Log.i( - "$TAG Looking for audio endpoint with type [${routes.first()}] for call with ID [$callId]" - ) - val callControlCallback = map[callId] - if (callControlCallback == null) { - Log.w("$TAG Failed to find callbacks for call with ID [$callId]") - return false - } - - return callControlCallback.applyAudioRouteToCallWithId(routes) - } } diff --git a/app/src/main/java/org/linphone/utils/AudioUtils.kt b/app/src/main/java/org/linphone/utils/AudioUtils.kt index 3ac0268cfd..83762908c2 100644 --- a/app/src/main/java/org/linphone/utils/AudioUtils.kt +++ b/app/src/main/java/org/linphone/utils/AudioUtils.kt @@ -55,6 +55,11 @@ class AudioUtils { routeAudioTo(call, arrayListOf(AudioDevice.Type.HearingAid)) } + @WorkerThread + fun routeAudioToEitherBluetoothOrHearingAid(call: Call? = null) { + routeAudioTo(call, arrayListOf(AudioDevice.Type.Bluetooth, AudioDevice.Type.HearingAid)) + } + @WorkerThread fun routeAudioToHeadset(call: Call? = null) { routeAudioTo( @@ -85,8 +90,7 @@ class AudioUtils { private fun applyAudioRouteChange( call: Call?, types: List, - output: Boolean = true, - skipTelecom: Boolean = false + output: Boolean = true ) { val currentCall = if (coreContext.core.callsNb > 0) { call ?: coreContext.core.currentCall ?: coreContext.core.calls[0] @@ -94,22 +98,7 @@ class AudioUtils { Log.w("$TAG No call found, setting audio route on Core") null } - - if (!skipTelecom) { - val callId = currentCall?.callLog?.callId.orEmpty() - Log.i("$TAG Trying to change audio endpoint using Telecom Manager APIs") - val success = coreContext.telecomManager.applyAudioRouteToCallWithId(types, callId) - if (!success) { - Log.w("$TAG Failed to change audio endpoint to [$types] for call ID [$callId]") - applyAudioRouteChange(currentCall, types, output, skipTelecom = true) - } else { - Log.i("$TAG It seems audio endpoint update using Telecom Manager was successful") - return - } - } else { - Log.i("$TAG Trying to change audio endpoint directly in Linphone SDK") - applyAudioRouteChangeInLinphone(currentCall, types, output) - } + applyAudioRouteChangeInLinphone(currentCall, types, output) } fun applyAudioRouteChangeInLinphone( From 93e26f6c1078aeb8407c376a525a714d1bebc3b9 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 18 Nov 2025 10:11:36 +0100 Subject: [PATCH 345/593] Fixed missing error toast when starting a group call/meeting and there's an error after adding participants --- .../AbstractConversationViewModel.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/AbstractConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/AbstractConversationViewModel.kt index 37759a39fb..fb1d6ee41b 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/AbstractConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/AbstractConversationViewModel.kt @@ -28,6 +28,8 @@ import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R import org.linphone.core.Address import org.linphone.core.ChatRoom +import org.linphone.core.Conference +import org.linphone.core.ConferenceListenerStub import org.linphone.core.MediaDirection import org.linphone.core.tools.Log import org.linphone.ui.GenericViewModel @@ -51,6 +53,23 @@ abstract class AbstractConversationViewModel : GenericViewModel() { lateinit var conversationId: String + private val conferenceListener = object : ConferenceListenerStub() { + @WorkerThread + override fun onStateChanged(conference: Conference, newState: Conference.State?) { + Log.i("$TAG Conference state changed [$newState]") + when (newState) { + Conference.State.CreationFailed -> { + showRedToast(R.string.conference_failed_to_create_group_call_toast, R.drawable.warning_circle) + conference.removeListener(this) + } + Conference.State.Created -> { + conference.removeListener(this) + } + else -> {} + } + } + } + fun isChatRoomInitialized(): Boolean { return ::chatRoom.isInitialized } @@ -173,6 +192,8 @@ abstract class AbstractConversationViewModel : GenericViewModel() { if (conference.inviteParticipants(participants, callParams) != 0) { Log.e("$TAG Failed to invite participants into group call!") showRedToast(R.string.conference_failed_to_create_group_call_toast, R.drawable.warning_circle) + } else { + conference.addListener(conferenceListener) } } } From e290a8c4ea33bd2fdeb07386bdf225528a52c941 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 18 Nov 2025 10:43:12 +0100 Subject: [PATCH 346/593] Added resources shrink to release build --- app/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d77c8ed368..a6bee3d109 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -169,6 +169,7 @@ android { getByName("release") { isMinifyEnabled = true + isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", From 41e6776b32a055240d541fa36f3b5ba1598f5717 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 17 Nov 2025 13:40:54 +0100 Subject: [PATCH 347/593] Use newly available API to properly remove account and all associated data --- .../viewmodel/AccountProfileViewModel.kt | 40 +------------------ 1 file changed, 2 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt index 6f1159abc8..cf09fa1807 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt @@ -23,10 +23,7 @@ import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import java.io.File import java.util.Locale -import kotlinx.coroutines.launch import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R @@ -45,7 +42,6 @@ import org.linphone.ui.main.model.AccountModel import org.linphone.ui.main.model.isEndToEndEncryptionMandatory import org.linphone.ui.main.settings.model.AccountDeviceModel import org.linphone.utils.Event -import org.linphone.utils.FileUtils class AccountProfileViewModel @UiThread @@ -268,41 +264,9 @@ class AccountProfileViewModel fun deleteAccount() { coreContext.postOnCoreThread { core -> if (::account.isInitialized) { - Log.i("$TAG Removing call logs, conversations & meetings related to account being removed") - account.clearCallLogs() - - // Wait for a better API in the SDK, deleteChatRoom will cause user to leave the groups, - // which will cause issues in case of multi device - /* - for (conversation in account.chatRooms) { - core.deleteChatRoom(conversation) - } - */ - for (meeting in account.conferenceInformationList) { - core.deleteConferenceInformation(meeting) - } - val identity = account.params.identityAddress?.asStringUriOnly() - val authInfo = account.findAuthInfo() - if (authInfo != null) { - Log.i("$TAG Found auth info for account [$identity], removing it") - if (authInfo.password.isNullOrEmpty() && authInfo.ha1.isNullOrEmpty() && authInfo.accessToken != null) { - Log.i("$TAG Auth info was using bearer token instead of password") - val ssoCache = File(corePreferences.ssoCacheFile) - if (ssoCache.exists()) { - Log.i("$TAG Found auth_state.json file, deleting it") - viewModelScope.launch { - FileUtils.deleteFile(ssoCache.absolutePath) - } - } - } - core.removeAuthInfo(authInfo) - } else { - Log.w("$TAG Failed to find matching auth info for account [$identity]") - } - - core.removeAccount(account) - Log.i("$TAG Account [$identity] has been removed") + Log.w("$TAG Removing account [$identity] and all related data (auth info, conferences, conversations, call logs)") + core.removeAccountWithData(account) accountRemovedEvent.postValue(Event(true)) } } From 5ed68e0171850ff2300b799196062cceb788e151 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 18 Nov 2025 14:38:22 +0100 Subject: [PATCH 348/593] Remove AuthInfo when creating CardDAV entry if synchronization fails --- .../settings/viewmodel/CardDavViewModel.kt | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/CardDavViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/CardDavViewModel.kt index a388b4badf..8f93a81f47 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/CardDavViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/CardDavViewModel.kt @@ -26,6 +26,7 @@ import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.contacts.ContactLoader.Companion.LINPHONE_ADDRESS_BOOK_FRIEND_LIST +import org.linphone.core.AuthInfo import org.linphone.core.Factory import org.linphone.core.FriendList import org.linphone.core.FriendListListenerStub @@ -60,6 +61,8 @@ class CardDavViewModel val isReadOnly = MutableLiveData() + var pendingAuthInfo: AuthInfo? = null + val syncSuccessfulEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -109,9 +112,16 @@ class CardDavViewModel syncInProgress.postValue(false) showRedToast(R.string.settings_contacts_carddav_sync_error_toast, R.drawable.warning_circle) if (isEdit.value == false) { - Log.e("$TAG Synchronization failed, removing Friend list from Core") + Log.e("$TAG Synchronization failed, removing Friend list & AuthInfo from Core") friendList.removeListener(this) coreContext.core.removeFriendList(friendList) + + val authInfo = pendingAuthInfo + if (authInfo != null) { + Log.i("$TAG Removing pending auth info [${authInfo.username}] with realm [${authInfo.realm}]") + coreContext.core.removeAuthInfo(authInfo) + pendingAuthInfo = null + } } } else -> {} @@ -147,6 +157,7 @@ class CardDavViewModel friendList = found friendList.addListener(friendListListener) isReadOnly.postValue(friendList.isReadOnly) + pendingAuthInfo = null displayName.postValue(name) storeNewContactsInIt.postValue( @@ -168,6 +179,14 @@ class CardDavViewModel ) corePreferences.friendListInWhichStoreNewlyCreatedFriends = LINPHONE_ADDRESS_BOOK_FRIEND_LIST } + + val authInfo = pendingAuthInfo + if (authInfo != null) { + Log.i("$TAG Removing pending auth info [${authInfo.username}] with realm [${authInfo.realm}]") + core.removeAuthInfo(authInfo) + pendingAuthInfo = null + } + core.removeFriendList(friendList) Log.i("$TAG Removed friends list with display name [$name]") showGreenToast(R.string.settings_contacts_carddav_deleted_toast, R.drawable.trash_simple) @@ -224,6 +243,7 @@ class CardDavViewModel authRealm, null ) + pendingAuthInfo = authInfo core.addAuthInfo(authInfo) } From bc7ac8be64a7ccf128c28ace286e9978fbbce75c Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 18 Nov 2025 17:12:44 +0100 Subject: [PATCH 349/593] Fixed issue with recording player fragment layout using lateinit property model --- .../viewmodel/RecordingMediaPlayerViewModel.kt | 13 +++++++++++++ .../main/res/layout/recording_player_fragment.xml | 8 ++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt index 2440e7de5b..2959a87646 100644 --- a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt @@ -59,6 +59,14 @@ class RecordingMediaPlayerViewModel val isUsingSmffFileFormat = MutableLiveData() + val formattedDuration = MutableLiveData() + + val duration = MutableLiveData() + + val displayName = MutableLiveData() + + val dateTime = MutableLiveData() + private var audioFocusRequest: AudioFocusRequestCompat? = null private val playerListener = PlayerListener { @@ -90,6 +98,11 @@ class RecordingMediaPlayerViewModel fun loadRecording(model: RecordingModel) { recordingModel = model + formattedDuration.postValue(model.formattedDuration) + duration.postValue(model.duration) + displayName.postValue(model.displayName) + dateTime.postValue(model.dateTime) + coreContext.postOnCoreThread { core -> isUsingSmffFileFormat.postValue(model.filePath.endsWith(LinphoneUtils.RECORDING_SMFF_FILE_EXTENSION)) initPlayer() diff --git a/app/src/main/res/layout/recording_player_fragment.xml b/app/src/main/res/layout/recording_player_fragment.xml index fcc6e2ab21..173572b734 100644 --- a/app/src/main/res/layout/recording_player_fragment.xml +++ b/app/src/main/res/layout/recording_player_fragment.xml @@ -68,7 +68,7 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:max="@{viewModel.recordingModel.duration, default=100}" + android:max="@{viewModel.duration, default=100}" android:progress="@{viewModel.position, default=75}" app:trackCornerRadius="5dp" app:trackThickness="10dp" @@ -87,7 +87,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="16dp" - android:text="@{viewModel.recordingModel.formattedDuration, default=`00:42`}" + android:text="@{viewModel.formattedDuration, default=`00:42`}" android:textSize="13sp" android:textColor="@color/bc_white" app:layout_constraintTop_toTopOf="@id/play_pause_audio_playback" @@ -130,7 +130,7 @@ android:layout_height="wrap_content" android:layout_marginStart="10dp" android:layout_marginEnd="10dp" - android:text="@{viewModel.recordingModel.displayName, default=`nomdufichier.jpg`}" + android:text="@{viewModel.displayName, default=`nomdufichier.jpg`}" android:textSize="13sp" android:textColor="@color/gray_main2_600" android:maxLines="1" @@ -148,7 +148,7 @@ android:layout_height="wrap_content" android:layout_marginStart="10dp" android:layout_marginEnd="10dp" - android:text="@{viewModel.recordingModel.dateTime, default=`envoyé le 02/05/2023 à 11h05`}" + android:text="@{viewModel.dateTime, default=`envoyé le 02/05/2023 à 11h05`}" android:textSize="12sp" android:textColor="@color/gray_main2_500" android:maxLines="1" From 170cd6fccc40426d25cc104f290673871c7dfc81 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 20 Nov 2025 15:50:56 +0100 Subject: [PATCH 350/593] Cancel voice message recording when starting editing already sent text message --- .../main/chat/viewmodel/SendMessageInConversationViewModel.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt index 0e9cac65c0..4b72bf66f4 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt @@ -239,6 +239,9 @@ class SendMessageInConversationViewModel if (isReplying.value == true) { cancelReply() } + if (isVoiceRecording.value == true) { + cancelVoiceMessageRecording() + } val newValue = model.text.value?.toString() ?: "" textToSend.value = newValue From 1183a9e1c23cbf797c466c27c1dfc794d93347c9 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 21 Nov 2025 10:01:34 +0100 Subject: [PATCH 351/593] Updated CHANGELOG & version code from release/6.0 branch --- CHANGELOG.md | 14 +++++++++++++- app/build.gradle.kts | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3a2dafcdb..c0c565cb48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ Group changes to describe their impact on the project, as follows: - Added support for HDMI audio devices ### Changed +- No longer follow TelecomManager audio endpoint during calls, using our own routing policy +- Removing an account will also remove all related data in the local database (auth info, call logs, conversations, meetings, etc...) - Hide SIP address/phone number picker dialog if contact has exactly one SIP address matching both the app default domain & the currently selected account domain - Improved UI on tablets with screen sw600dp and higher, will look more like our desktop app - Now loading media/documents contents in conversation by chunks (instead of all of them at once) @@ -39,9 +41,19 @@ Group changes to describe their impact on the project, as follows: - Increased shared media preview size in chat - Un-encrypted conversation warning will be more visible for accounts that support end-to-end encrypted conversations - Made numpad buttons larger by changing their shape -- All LDAP fields are mandatory now, added toggle to choose wether or not to print LDAP logs +- All LDAP fields are mandatory now - Permission fragment will only show missing ones +## [6.0.20] - 2025-11-21 + +### Changed +- Added shrink resources to release config in gradle + +### Fixed +- Remove AuthInfo when configuring a CardDAV friend list if synchronization fails +- Added missing toast when starting a group call or meeting if there's an issue +- Fixed crash in RecordingPlayerFragment due to used lateinit property before it's initialized + ## [6.0.19] - 2025-10-16 ### Added diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a6bee3d109..9eea965da7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -100,7 +100,7 @@ android { applicationId = packageName minSdk = 28 targetSdk = 36 - versionCode = 600019 // 6.00.019 + versionCode = 600020 // 6.00.020 versionName = "6.1.0-alpha" manifestPlaceholders["appAuthRedirectScheme"] = packageName From c4965450233b6f8f94ea257f0c49c230c9abf43d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 21 Nov 2025 16:31:00 +0100 Subject: [PATCH 352/593] Added missing try/catch around some startActivity to prevent not found exceptions --- .../org/linphone/compatibility/Api34Compatibility.kt | 7 ++++++- .../org/linphone/ui/fileviewer/FileViewerActivity.kt | 6 +++++- .../linphone/ui/fileviewer/MediaViewerActivity.kt | 7 ++++++- .../ui/main/contacts/fragment/ContactFragment.kt | 12 ++++++++++-- .../main/contacts/fragment/ContactsListFragment.kt | 7 ++++++- .../fragment/RecordingMediaPlayerFragment.kt | 7 ++++++- .../recordings/fragment/RecordingsListFragment.kt | 7 ++++++- 7 files changed, 45 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/linphone/compatibility/Api34Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api34Compatibility.kt index 5d86ac7b77..17eed9de73 100644 --- a/app/src/main/java/org/linphone/compatibility/Api34Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api34Compatibility.kt @@ -24,6 +24,7 @@ import android.app.Notification import android.app.NotificationManager import android.app.PendingIntent import android.app.Service +import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.os.Build @@ -74,7 +75,11 @@ class Api34Compatibility { intent.data = "package:${context.packageName}".toUri() intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) Log.i("$TAG Starting ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT") - context.startActivity(intent, null) + try { + context.startActivity(intent, null) + } catch (anfe: ActivityNotFoundException) { + Log.e("$TAG Failed to start intent for granting full screen intent permission: $anfe") + } } fun sendPendingIntent(pendingIntent: PendingIntent, bundle: Bundle) { diff --git a/app/src/main/java/org/linphone/ui/fileviewer/FileViewerActivity.kt b/app/src/main/java/org/linphone/ui/fileviewer/FileViewerActivity.kt index 7035d4df29..ed245c277a 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/FileViewerActivity.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/FileViewerActivity.kt @@ -197,7 +197,11 @@ class FileViewerActivity : GenericActivity() { } val shareIntent = Intent.createChooser(sendIntent, null) - startActivity(shareIntent) + try { + startActivity(shareIntent) + } catch (anfe: ActivityNotFoundException) { + Log.e("$TAG Failed to start intent chooser: $anfe") + } } else { Log.e("$TAG Failed to copy file [$filePath] to share!") } diff --git a/app/src/main/java/org/linphone/ui/fileviewer/MediaViewerActivity.kt b/app/src/main/java/org/linphone/ui/fileviewer/MediaViewerActivity.kt index 12bb3b8fb5..08fcaee5fd 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/MediaViewerActivity.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/MediaViewerActivity.kt @@ -1,5 +1,6 @@ package org.linphone.ui.fileviewer +import android.content.ActivityNotFoundException import android.content.Intent import android.os.Bundle import androidx.activity.enableEdgeToEdge @@ -269,7 +270,11 @@ class MediaViewerActivity : GenericActivity() { } val shareIntent = Intent.createChooser(sendIntent, null) - startActivity(shareIntent) + try { + startActivity(shareIntent) + } catch (anfe: ActivityNotFoundException) { + Log.e("$TAG Failed to start intent chooser: $anfe") + } } else { Log.e( "$TAG Failed to copy file [$filePath] to share!" diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt index 164913efb6..53f0d3aeae 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt @@ -283,7 +283,11 @@ class ContactFragment : SlidingPaneChildFragment() { } val shareIntent = Intent.createChooser(sendIntent, null) - startActivity(shareIntent) + try { + startActivity(shareIntent) + } catch (anfe: ActivityNotFoundException) { + Log.e("$TAG Failed to start intent chooser: $anfe") + } } private fun inviteContactBySms(number: String) { @@ -299,7 +303,11 @@ class ContactFragment : SlidingPaneChildFragment() { putExtra("address", number) putExtra("sms_body", smsBody) } - startActivity(smsIntent) + try { + startActivity(smsIntent) + } catch (anfe: ActivityNotFoundException) { + Log.e("$TAG Failed to start SMS intent: $anfe") + } } private fun showTrustProcessDialog() { diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt index 0cea2cdf15..8465ba3cb6 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt @@ -20,6 +20,7 @@ package org.linphone.ui.main.contacts.fragment import android.Manifest +import android.content.ActivityNotFoundException import android.content.Intent import android.content.pm.PackageManager import android.os.Bundle @@ -333,7 +334,11 @@ class ContactsListFragment : AbstractMainFragment() { } val shareIntent = Intent.createChooser(sendIntent, null) - startActivity(shareIntent) + try { + startActivity(shareIntent) + } catch (anfe: ActivityNotFoundException) { + Log.e("$TAG Failed to start intent chooser: $anfe") + } } private fun showFilterPopupMenu(view: View) { diff --git a/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingMediaPlayerFragment.kt b/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingMediaPlayerFragment.kt index 9d78cc2100..956940cb9a 100644 --- a/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingMediaPlayerFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingMediaPlayerFragment.kt @@ -19,6 +19,7 @@ */ package org.linphone.ui.main.recordings.fragment +import android.content.ActivityNotFoundException import android.content.Intent import android.graphics.SurfaceTexture import android.os.Bundle @@ -194,7 +195,11 @@ class RecordingMediaPlayerFragment : GenericMainFragment() { } val shareIntent = Intent.createChooser(sendIntent, null) - startActivity(shareIntent) + try { + startActivity(shareIntent) + } catch (anfe: ActivityNotFoundException) { + Log.e("$TAG Failed to start intent chooser: $anfe") + } } } } diff --git a/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsListFragment.kt b/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsListFragment.kt index 26405911d4..a152a848d0 100644 --- a/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsListFragment.kt @@ -19,6 +19,7 @@ */ package org.linphone.ui.main.recordings.fragment +import android.content.ActivityNotFoundException import android.content.Intent import android.os.Bundle import android.view.LayoutInflater @@ -227,7 +228,11 @@ class RecordingsListFragment : GenericMainFragment() { } val shareIntent = Intent.createChooser(sendIntent, null) - startActivity(shareIntent) + try { + startActivity(shareIntent) + } catch (anfe: ActivityNotFoundException) { + Log.e("$TAG Failed to start intent chooser: $anfe") + } } } } From 85aa50d8d8c058b4d5a314a8d6da1263a35171b3 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 24 Nov 2025 10:34:02 +0100 Subject: [PATCH 353/593] Fixed in-call media encryption alignment --- app/src/main/res/layout-land/call_active_fragment.xml | 2 +- app/src/main/res/layout/call_active_conference_fragment.xml | 2 +- app/src/main/res/layout/call_active_fragment.xml | 2 +- app/src/main/res/layout/call_media_encryption_info.xml | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/layout-land/call_active_fragment.xml b/app/src/main/res/layout-land/call_active_fragment.xml index edd0199a16..b3e63784d3 100644 --- a/app/src/main/res/layout-land/call_active_fragment.xml +++ b/app/src/main/res/layout-land/call_active_fragment.xml @@ -254,7 +254,7 @@ bind:viewModel="@{viewModel}" bind:callMediaEncryptionStatisticsClickListener="@{callMediaEncryptionStatisticsClickListener}" app:layout_constraintTop_toBottomOf="@id/name" - app:layout_constraintStart_toEndOf="@id/back" + app:layout_constraintStart_toStartOf="@id/name" app:layout_constraintEnd_toEndOf="parent"/> + android:layout_height="wrap_content"> Date: Mon, 24 Nov 2025 11:35:21 +0100 Subject: [PATCH 354/593] Added more info to startup listener, also log 3 previous startup reasons --- .../compatibility/Api35Compatibility.kt | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/linphone/compatibility/Api35Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api35Compatibility.kt index 13a9208693..d87066df41 100644 --- a/app/src/main/java/org/linphone/compatibility/Api35Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api35Compatibility.kt @@ -39,18 +39,42 @@ class Api35Compatibility { Executors.newSingleThreadExecutor() ) { info -> Log.i("==== Current startup information dump ====") - Log.i("TYPE = ${startupTypeToString(info.startType)}") - Log.i("STATE = ${startupStateToString(info.startupState)}") - Log.i("REASON = ${startupReasonToString(info.reason)}") - Log.i("FORCE STOPPED = ${if (info.wasForceStopped()) "yes" else "no"}") - Log.i("PROCESS NAME = ${info.processName}") - Log.i("=========================================") + logAppStartupInfo(info) + } + + Log.i("==== Fetching last three startup reasons if available ====") + val lastStartupInfo = activityManager.getHistoricalProcessStartReasons(3) + for (info in lastStartupInfo) { + Log.i("==== Previous startup information dump ====") + logAppStartupInfo(info) } } catch (iae: IllegalArgumentException) { Log.e("$TAG Can't add application start info completion listener: $iae") } } + private fun logAppStartupInfo(info: ApplicationStartInfo) { + Log.i("TYPE = ${startupTypeToString(info.startType)}") + Log.i("STATE = ${startupStateToString(info.startupState)}") + Log.i("REASON = ${startupReasonToString(info.reason)}") + Log.i("START COMPONENT = ${startComponentToString(info.launchMode)}") + Log.i("INTENT = ${info.intent}") + Log.i("FORCE STOPPED = ${if (info.wasForceStopped()) "yes" else "no"}") + Log.i("PROCESS NAME = ${info.processName}") + Log.i("=========================================") + } + + private fun startComponentToString(component: Int): String { + return when (component) { + ApplicationStartInfo.START_COMPONENT_ACTIVITY -> "Activity" + ApplicationStartInfo.START_COMPONENT_BROADCAST -> "Broadcast" + ApplicationStartInfo.START_COMPONENT_CONTENT_PROVIDER -> "Content Provider" + ApplicationStartInfo.START_COMPONENT_SERVICE -> "Service" + ApplicationStartInfo.START_COMPONENT_OTHER -> "Other" + else -> "Unexpected ($component)" + } + } + private fun startupTypeToString(type: Int): String { return when (type) { ApplicationStartInfo.START_TYPE_COLD -> "Cold" From 88e474533eaaece4d196cfaaa078c96745237a4c Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 24 Nov 2025 12:43:56 +0100 Subject: [PATCH 355/593] Fixed reply preview when a message has been deleted (locally or remotely) --- .../chat/viewmodel/ConversationViewModel.kt | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt index b11aecb9e5..797db9f8d6 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt @@ -308,12 +308,7 @@ class ConversationViewModel @WorkerThread override fun onMessageRetracted(chatRoom: ChatRoom, message: ChatMessage) { - for (model in eventsList.reversed()) { - if (model.model is MessageModel && model.model.replyToMessageId == message.messageId) { - model.model.computeReplyInfo() - break - } - } + updateRepliesUpTo(message) if (message.isOutgoing) { messageDeletedEvent.postValue(Event(true)) @@ -322,12 +317,7 @@ class ConversationViewModel @WorkerThread override fun onMessageContentEdited(chatRoom: ChatRoom, message: ChatMessage) { - for (model in eventsList.reversed()) { - if (model.model is MessageModel && model.model.replyToMessageId == message.messageId) { - model.model.computeReplyInfo() - break - } - } + updateRepliesUpTo(message) } } @@ -464,7 +454,9 @@ class ConversationViewModel Log.i("$TAG Removing chat message id [${chatMessageModel.id}] from events list") list.remove(found) + eventsList = list + updateEvents.postValue(Event(true)) isEmpty.postValue(eventsList.isEmpty()) } else { @@ -476,6 +468,8 @@ class ConversationViewModel Log.i("$TAG Deleting message id [${chatMessageModel.id}] from database") chatRoom.deleteMessage(chatMessageModel.chatMessage) messageDeletedEvent.postValue(Event(true)) + + updateRepliesUpTo(chatMessageModel.chatMessage) } } @@ -937,6 +931,21 @@ class ConversationViewModel } } + @WorkerThread + private fun updateRepliesUpTo(chatMessage: ChatMessage) { + for (model in eventsList.reversed()) { + if (model.model is MessageModel) { + if (model.model.replyToMessageId == chatMessage.messageId) { + model.model.computeReplyInfo() + } + + if (model.model.timestamp < chatMessage.time) { + break + } + } + } + } + @WorkerThread private fun computeComposingLabel() { if (!isChatRoomInitialized()) return From 696a593cbce61243d68b47e00479472e7890afd1 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 24 Nov 2025 15:20:48 +0100 Subject: [PATCH 356/593] Prevent replying to retracted message with swipe action --- .../ui/main/chat/fragment/ConversationFragment.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 8e06bd6685..7de0e4b6fb 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -474,9 +474,13 @@ open class ConversationFragment : SlidingPaneChildFragment() { val chatMessageEventLog = adapter.currentList[index] val chatMessageModel = (chatMessageEventLog.model as? MessageModel) if (chatMessageModel != null) { - sendMessageViewModel.replyToMessage(chatMessageModel) - // Open keyboard & focus edit text - binding.sendArea.messageToSend.showKeyboard() + if (chatMessageModel.hasBeenRetracted.value == true) { // Don't allow to reply to retracted messages + // TODO: notify user? + } else { + sendMessageViewModel.replyToMessage(chatMessageModel) + // Open keyboard & focus edit text + binding.sendArea.messageToSend.showKeyboard() + } } else { Log.e( "$TAG Can't reply, failed to get a ChatMessageModel from adapter item #[$index]" From 6bcce4ddbfef36cc6723cdcc7cc6b24a20712e9b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 25 Nov 2025 12:57:04 +0100 Subject: [PATCH 357/593] Fixed call recording wrong indicator in case UPDATE isn't answered --- .../org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 7ad6865378..07353b2ab7 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -855,15 +855,16 @@ class CurrentCallViewModel fun toggleRecording() { coreContext.postOnCoreThread { if (::currentCall.isInitialized) { - if (currentCall.params.isRecording) { + val recording = if (currentCall.params.isRecording) { Log.i("$TAG Stopping call recording") currentCall.stopRecording() + false } else { Log.i("$TAG Starting call recording") currentCall.startRecording() + true } - val recording = currentCall.params.isRecording isRecording.postValue(recording) if (recording) { showRecordingToast() From cc1cc7d929c445cc54ebdc8c9e50b363281ba546 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 25 Nov 2025 13:31:46 +0100 Subject: [PATCH 358/593] Fixed crash seen on Crashlytics due to clipboard data text being null --- .../ui/assistant/fragment/RegisterCodeConfirmationFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterCodeConfirmationFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterCodeConfirmationFragment.kt index 083a7c80e2..fa033c07b2 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterCodeConfirmationFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterCodeConfirmationFragment.kt @@ -80,7 +80,7 @@ class RegisterCodeConfirmationFragment : GenericFragment() { clipboard.addPrimaryClipChangedListener { val data = clipboard.primaryClip if (data != null && data.itemCount > 0) { - val clip = data.getItemAt(0).text.toString() + val clip = data.getItemAt(0).text?.toString() ?: "" if (clip.length == 4) { Log.i( "$TAG Found 4 digits [$clip] as primary clip in clipboard, using it and clear it" From c99acbb5e177b1c175880bcf6cee20447e8b5cc0 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 1 Dec 2025 10:55:57 +0100 Subject: [PATCH 359/593] Added missing update unread chat message count when a message has been retracted --- .../org/linphone/ui/main/chat/model/ConversationModel.kt | 1 + .../ui/main/chat/viewmodel/ConversationViewModel.kt | 2 ++ .../main/java/org/linphone/ui/main/model/AccountModel.kt | 6 ++++++ .../linphone/ui/main/viewmodel/AbstractMainViewModel.kt | 5 +++++ .../java/org/linphone/ui/main/viewmodel/MainViewModel.kt | 8 ++++++++ 5 files changed, 22 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt index 6e6a56c45a..3cf3298223 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt @@ -178,6 +178,7 @@ class ConversationModel Log.i("$TAG Last message [${message.messageId}] has been retracted") updateLastMessage() } + unreadMessageCount.postValue(chatRoom.unreadMessagesCount) } @WorkerThread diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt index 797db9f8d6..ea71e1f1cf 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt @@ -313,6 +313,8 @@ class ConversationViewModel if (message.isOutgoing) { messageDeletedEvent.postValue(Event(true)) } + + unreadMessagesCount.postValue(chatRoom.unreadMessagesCount) } @WorkerThread diff --git a/app/src/main/java/org/linphone/ui/main/model/AccountModel.kt b/app/src/main/java/org/linphone/ui/main/model/AccountModel.kt index 65100beb57..776654f033 100644 --- a/app/src/main/java/org/linphone/ui/main/model/AccountModel.kt +++ b/app/src/main/java/org/linphone/ui/main/model/AccountModel.kt @@ -81,6 +81,7 @@ class AccountModel update() } + @WorkerThread override fun onMessageWaitingIndicationChanged( account: Account, mwi: MessageWaitingIndication @@ -110,6 +111,11 @@ class AccountModel computeNotificationsCount() } + @WorkerThread + override fun onMessageRetracted(core: Core, chatRoom: ChatRoom, message: ChatMessage) { + computeNotificationsCount() + } + @WorkerThread override fun onMessagesReceived( core: Core, diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractMainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractMainViewModel.kt index e81993a003..0e0cba03d3 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractMainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/AbstractMainViewModel.kt @@ -140,6 +140,11 @@ open class AbstractMainViewModel computeUnreadMessagesCount() } + @WorkerThread + override fun onMessageRetracted(core: Core, chatRoom: ChatRoom, message: ChatMessage) { + computeUnreadMessagesCount() + } + @WorkerThread override fun onGlobalStateChanged(core: Core, state: GlobalState?, message: String) { if (core.globalState == GlobalState.On) { diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt index 0d84839f42..afb6275427 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt @@ -197,6 +197,14 @@ class MainViewModel } } + @WorkerThread + override fun onMessageRetracted(core: Core, chatRoom: ChatRoom, message: ChatMessage) { + val account = LinphoneUtils.getAccountForAddress(chatRoom.localAddress) + if (account != null && account != core.defaultAccount) { + computeNonDefaultAccountNotificationsCount() + } + } + @WorkerThread override fun onNetworkReachable(core: Core, reachable: Boolean) { Log.i( From 3ffda24b828e971f1fb5a571f9bed4568b9af0fa Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 1 Dec 2025 10:11:31 +0100 Subject: [PATCH 360/593] Workaround missing audio focus requests & wrong audio manager mode when TelecomManager APIs aren't available on device --- .../org/linphone/telecom/TelecomManager.kt | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/linphone/telecom/TelecomManager.kt b/app/src/main/java/org/linphone/telecom/TelecomManager.kt index 4a9867f926..15e89a8a76 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomManager.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomManager.kt @@ -68,15 +68,14 @@ class TelecomManager } } + private val hasTelecomFeature = context.packageManager.hasSystemFeature("android.software.telecom") + private var currentlyFollowedCalls: Int = 0 init { - val hasTelecomFeature = - context.packageManager.hasSystemFeature("android.software.telecom") Log.i( "$TAG android.software.telecom feature is [${if (hasTelecomFeature) "available" else "not available"}]" ) - try { callsManager.registerAppWithTelecom( CallsManager.CAPABILITY_BASELINE or @@ -194,12 +193,21 @@ class TelecomManager @WorkerThread fun onCoreStarted(core: Core) { Log.i("$TAG Core has been started") - core.addListener(coreListener) + if (hasTelecomFeature) { + core.addListener(coreListener) + } else { + Log.w( + "$TAG android.software.telecom feature is not available, enable audio focus requests in Linphone SDK" + ) + coreContext.core.config.setBool("audio", "android_disable_audio_focus_requests", false) + } } @WorkerThread fun onCoreStopped(core: Core) { Log.i("$TAG Core is being stopped") - core.removeListener(coreListener) + if (hasTelecomFeature) { + core.removeListener(coreListener) + } } } From bf4b5a51f5804c833eca42f67c4dad00f1848887 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 2 Dec 2025 13:26:18 +0100 Subject: [PATCH 361/593] Added back largeHeap in Manifest XML file --- app/src/main/AndroidManifest.xml | 1 + app/src/main/java/org/linphone/telecom/TelecomManager.kt | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5bf85a42ef..bd5a3d7af1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -54,6 +54,7 @@ android:localeConfig="@xml/locales_config" android:theme="@style/Theme.Linphone" android:appCategory="social" + android:largeHeap="true" tools:targetApi="35"> diff --git a/app/src/main/java/org/linphone/telecom/TelecomManager.kt b/app/src/main/java/org/linphone/telecom/TelecomManager.kt index 15e89a8a76..39d1b9e203 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomManager.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomManager.kt @@ -79,7 +79,7 @@ class TelecomManager try { callsManager.registerAppWithTelecom( CallsManager.CAPABILITY_BASELINE or - CallsManager.Companion.CAPABILITY_SUPPORTS_VIDEO_CALLING + CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING ) Log.i("$TAG App has been registered with Telecom") } catch (e: Exception) { @@ -112,9 +112,9 @@ class TelecomManager val isVideo = LinphoneUtils.isVideoEnabled(call) val type = if (isVideo) { - CallAttributesCompat.Companion.CALL_TYPE_VIDEO_CALL + CallAttributesCompat.CALL_TYPE_VIDEO_CALL } else { - CallAttributesCompat.Companion.CALL_TYPE_AUDIO_CALL + CallAttributesCompat.CALL_TYPE_AUDIO_CALL } scope.launch { From 7817e6603ca38379b0e0e86912b092de8fd14e44 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 3 Dec 2025 14:19:35 +0100 Subject: [PATCH 362/593] Force front camera as default device when leaving QR code fragment --- .../java/org/linphone/core/CoreContext.kt | 22 +++++++++++++++++++ .../fragment/QrCodeScannerFragment.kt | 2 ++ .../ui/assistant/viewmodel/QrCodeViewModel.kt | 22 +++++++------------ .../viewmodel/MeetingWaitingRoomViewModel.kt | 18 +++++++-------- 4 files changed, 40 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index f80055ac41..819964db39 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -1269,4 +1269,26 @@ class CoreContext } } } + + fun setBackCamera(): Boolean { + for (camera in core.videoDevicesList) { + if (camera.contains("Back")) { + Log.i("TAG Found back facing camera [$camera], using it") + coreContext.core.videoDevice = camera + return true + } + } + return false + } + + fun setFrontCamera(): Boolean { + for (camera in core.videoDevicesList) { + if (camera.contains("Front")) { + Log.i("$TAG Found front facing camera [$camera], using it") + coreContext.core.videoDevice = camera + return true + } + } + return false + } } diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt index 566d9094da..8a7c49b1ae 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt @@ -147,6 +147,8 @@ class QrCodeScannerFragment : GenericFragment() { core.nativePreviewWindowId = null core.isVideoPreviewEnabled = false core.isQrcodeVideoPreviewEnabled = false + + coreContext.setFrontCamera() } super.onPause() diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt index 6f986194e3..e0de49fbc9 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt @@ -122,23 +122,17 @@ class QrCodeViewModel // this is required right after granting the CAMERA permission core.reloadVideoDevices() - for (camera in core.videoDevicesList) { - if (camera.contains("Back")) { - Log.i("$TAG Found back facing camera [$camera], using it") - coreContext.core.videoDevice = camera - return@postOnCoreThread + if (!coreContext.setBackCamera()) { + for (camera in core.videoDevicesList) { + if (camera != "StaticImage: Static picture") { + Log.w("$TAG No back facing camera found, using first one available [$camera]") + coreContext.core.videoDevice = camera + return@postOnCoreThread + } } - } - for (camera in core.videoDevicesList) { - if (camera != "StaticImage: Static picture") { - Log.w("$TAG No back facing camera found, using first one available [$camera]") - coreContext.core.videoDevice = camera - return@postOnCoreThread - } + Log.e("$TAG No camera device found!") } - - Log.e("$TAG No camera device found!") } } } diff --git a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingWaitingRoomViewModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingWaitingRoomViewModel.kt index 2ad4511229..169191702e 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingWaitingRoomViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingWaitingRoomViewModel.kt @@ -212,18 +212,16 @@ class MeetingWaitingRoomViewModel @UiThread fun setFrontCamera() { coreContext.postOnCoreThread { core -> - for (camera in core.videoDevicesList) { - if (camera.contains("Front")) { - Log.i("$TAG Found front facing camera [$camera], using it") - coreContext.core.videoDevice = camera - return@postOnCoreThread + if (!coreContext.setFrontCamera()) { + for (camera in core.videoDevicesList) { + if (camera != "StaticImage: Static picture") { + Log.w("$TAG No front facing camera found, using first one available [$camera]") + coreContext.core.videoDevice = camera + return@postOnCoreThread + } } - } - val first = core.videoDevicesList.firstOrNull() - if (first != null) { - Log.w("$TAG No front facing camera found, using first one available [$first]") - coreContext.core.videoDevice = first + Log.e("$TAG No camera device found!") } } } From e173e402c297c35868709c1c39cc323e66211c1d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 3 Dec 2025 10:27:21 +0100 Subject: [PATCH 363/593] Added answer/decline keyboard shortcuts to CallActivity --- CHANGELOG.md | 6 +++ .../java/org/linphone/ui/call/CallActivity.kt | 51 ++++++++++++++++++- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0c565cb48..69119f8aa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Group changes to describe their impact on the project, as follows: ### Added - Added the ability to edit/delete chat messages sent less than 24 hours ago. +- Added keyboard shortcuts on IncomingCallFragment: Ctrl + Shift + A to answer the call, Ctrl + Shift + D to decline it - Added PDF preview in conversation (message bubble & documents list) - Added hover effect when using a mouse (useful for tablets or devices with desktop mode) - Support right click on some items to open bottom sheet/menu @@ -35,6 +36,7 @@ Group changes to describe their impact on the project, as follows: - Removing an account will also remove all related data in the local database (auth info, call logs, conversations, meetings, etc...) - Hide SIP address/phone number picker dialog if contact has exactly one SIP address matching both the app default domain & the currently selected account domain - Improved UI on tablets with screen sw600dp and higher, will look more like our desktop app +- Improved navigation within app when using a keyboard - Now loading media/documents contents in conversation by chunks (instead of all of them at once) - Simplified audio device name in settings - Reworked some settings (moved calls related ones from advanced settings to advanced calls settings) @@ -43,6 +45,10 @@ Group changes to describe their impact on the project, as follows: - Made numpad buttons larger by changing their shape - All LDAP fields are mandatory now - Permission fragment will only show missing ones +- Added more info into StartupListener logs + +### Fixed +- No audio focus & wrong audio manager mode when TelecomManager isn't supported by device ## [6.0.20] - 2025-11-21 diff --git a/app/src/main/java/org/linphone/ui/call/CallActivity.kt b/app/src/main/java/org/linphone/ui/call/CallActivity.kt index 4a0ff2b28c..3dc7324068 100644 --- a/app/src/main/java/org/linphone/ui/call/CallActivity.kt +++ b/app/src/main/java/org/linphone/ui/call/CallActivity.kt @@ -25,6 +25,10 @@ import android.content.pm.PackageManager import android.content.res.Resources import android.graphics.Color import android.os.Bundle +import android.view.KeyEvent +import android.view.KeyboardShortcutGroup +import android.view.KeyboardShortcutInfo +import android.view.Menu import androidx.activity.SystemBarStyle import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts @@ -66,6 +70,7 @@ import org.linphone.ui.call.viewmodel.CallsViewModel import org.linphone.ui.call.viewmodel.CurrentCallViewModel import org.linphone.ui.call.viewmodel.SharedCallViewModel import org.linphone.ui.main.MainActivity +import org.linphone.utils.AppUtils @UiThread class CallActivity : GenericActivity() { @@ -408,7 +413,51 @@ class CallActivity : GenericActivity() { } } - @UiThread + override fun onProvideKeyboardShortcuts( + data: MutableList?, + menu: Menu?, + deviceId: Int + ) { + super.onProvideKeyboardShortcuts(data, menu, deviceId) + + val keyboardShortcutGroup = KeyboardShortcutGroup( + "Answer/Decline incoming call", + listOf( + KeyboardShortcutInfo( + AppUtils.getString(R.string.call_action_answer), + KeyEvent.KEYCODE_A, + KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON + ), + KeyboardShortcutInfo( + AppUtils.getString(R.string.call_action_decline), + KeyEvent.KEYCODE_D, + KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON + ) + ) + ) + data?.add(keyboardShortcutGroup) + Log.i("$TAG Incoming call answer/decline shortcuts added") + } + + override fun onKeyShortcut(keyCode: Int, event: KeyEvent?): Boolean { + if (event?.isCtrlPressed == true && event.isShiftPressed) { + val navController = findNavController(R.id.call_nav_container) + if (navController.currentDestination?.id == R.id.incomingCallFragment) { + when (keyCode) { + KeyEvent.KEYCODE_A -> { + Log.i("$TAG Answer incoming call shortcut triggered") + callViewModel.answer() + } + KeyEvent.KEYCODE_D -> { + Log.i("$TAG Decline incoming call shortcut triggered") + callViewModel.hangUp() + } + } + } + } + return true + } + fun goToMainActivity() { if (isPipSupported && callViewModel.isVideoEnabled.value == true) { Log.i("$TAG User is going back to MainActivity, try entering PiP mode") From 40d195e06bb826711bc58743a3fb4a6cc5922c3b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 4 Dec 2025 11:26:46 +0100 Subject: [PATCH 364/593] Updated dependencies --- app/build.gradle.kts | 1 - gradle/libs.versions.toml | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9eea965da7..a1a8eb02f3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -213,7 +213,6 @@ android { dependencies { implementation(libs.androidx.annotations) - implementation(libs.androidx.activity) implementation(libs.androidx.appcompat) implementation(libs.androidx.constraint.layout) implementation(libs.androidx.core.ktx) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8e69563cc1..4130128bf9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,12 +3,11 @@ agp = "8.13.1" kotlin = "2.2.21" gmsGoogleServices = "4.4.4" firebaseCrashlytics = "3.0.6" -firebaseBomVersion = "34.5.0" +firebaseBomVersion = "34.6.0" #noinspection NewerVersionAvailable ktlint = "12.3.0" annotations = "1.9.1" -activity = "1.11.0" appcompat = "1.7.1" constraintLayout = "2.2.1" coreKtx = "1.17.0" @@ -16,9 +15,9 @@ splashscreen = "1.2.0" telecom = "1.0.1" media = "1.7.1" recyclerview = "1.4.0" -swipeRefreshLayout = "1.1.0" +swipeRefreshLayout = "1.2.0" slidingpanelayout = "1.2.0" -window = "1.5.0" +window = "1.5.1" gridlayout = "1.1.0" securityCryptoKtx = "1.1.0" navigation = "2.9.6" @@ -36,7 +35,6 @@ linphone = "5.5.+" [libraries] androidx-annotations = { group = "androidx.annotation", name = "annotation", version.ref = "annotations" } -androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } androidx-constraint-layout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintLayout" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } From 61c79a86f7dc1078b155fde51aed820fea28b97f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 5 Dec 2025 09:58:09 +0100 Subject: [PATCH 365/593] Added seek to recordings player & media player --- CHANGELOG.md | 1 + .../fragment/MediaViewerFragment.kt | 18 +++++++++++++++ .../ui/fileviewer/viewmodel/MediaViewModel.kt | 8 +++++++ .../fragment/RecordingMediaPlayerFragment.kt | 18 +++++++++++++++ .../RecordingMediaPlayerViewModel.kt | 20 +++++++++++++++++ .../org/linphone/utils/DataBindingUtils.kt | 6 +++++ .../res/drawable/media_player_seekbar.xml | 19 ++++++++++++++++ .../drawable/media_player_seekbar_thumb.xml | 10 +++++++++ .../file_media_viewer_child_fragment.xml | 22 +++++++++---------- .../res/layout/recording_player_fragment.xml | 22 +++++++++---------- 10 files changed, 122 insertions(+), 22 deletions(-) create mode 100644 app/src/main/res/drawable/media_player_seekbar.xml create mode 100644 app/src/main/res/drawable/media_player_seekbar_thumb.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 69119f8aa5..5096dd44eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Group changes to describe their impact on the project, as follows: ### Added - Added the ability to edit/delete chat messages sent less than 24 hours ago. - Added keyboard shortcuts on IncomingCallFragment: Ctrl + Shift + A to answer the call, Ctrl + Shift + D to decline it +- Added seeking feature to recordings & media player within app - Added PDF preview in conversation (message bubble & documents list) - Added hover effect when using a mouse (useful for tablets or devices with desktop mode) - Support right click on some items to open bottom sheet/menu diff --git a/app/src/main/java/org/linphone/ui/fileviewer/fragment/MediaViewerFragment.kt b/app/src/main/java/org/linphone/ui/fileviewer/fragment/MediaViewerFragment.kt index d3e8b3dd46..7d278161f1 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/fragment/MediaViewerFragment.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/fragment/MediaViewerFragment.kt @@ -26,6 +26,7 @@ import android.view.Surface import android.view.TextureView.SurfaceTextureListener import android.view.View import android.view.ViewGroup +import android.widget.SeekBar import androidx.annotation.UiThread import androidx.lifecycle.ViewModelProvider import org.linphone.core.tools.Log @@ -45,6 +46,21 @@ class MediaViewerFragment : GenericMainFragment() { private lateinit var viewModel: MediaViewModel + private val seekBarListener = object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + viewModel.pause() + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + val newPosition = seekBar.progress + viewModel.seekTo(newPosition) + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -87,6 +103,8 @@ class MediaViewerFragment : GenericMainFragment() { sharedViewModel.mediaViewerFullScreenMode.value = fullScreenMode } + binding.setSeekBarListener(seekBarListener) + viewModel.videoSizeChangedEvent.observe(viewLifecycleOwner) { it.consume { pair -> val width = pair.first diff --git a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/MediaViewModel.kt b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/MediaViewModel.kt index 39f524b9d9..9cbec6e521 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/MediaViewModel.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/MediaViewModel.kt @@ -161,6 +161,14 @@ class MediaViewModel } } + @UiThread + fun seekTo(position: Int) { + if (::mediaPlayer.isInitialized) { + mediaPlayer.seekTo(position) + play() + } + } + @UiThread private fun initMediaPlayer() { isMediaPlaying.value = false diff --git a/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingMediaPlayerFragment.kt b/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingMediaPlayerFragment.kt index 956940cb9a..79ba55950a 100644 --- a/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingMediaPlayerFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingMediaPlayerFragment.kt @@ -27,6 +27,7 @@ import android.view.LayoutInflater import android.view.TextureView import android.view.View import android.view.ViewGroup +import android.widget.SeekBar import androidx.core.content.FileProvider import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope @@ -52,6 +53,21 @@ class RecordingMediaPlayerFragment : GenericMainFragment() { private lateinit var viewModel: RecordingMediaPlayerViewModel + private val seekBarListener = object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + + } + + override fun onStartTrackingTouch(seekBar: SeekBar) { + viewModel.pause() + } + + override fun onStopTrackingTouch(seekBar: SeekBar) { + val newPosition = seekBar.progress + viewModel.seekTo(newPosition) + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -84,6 +100,8 @@ class RecordingMediaPlayerFragment : GenericMainFragment() { exportFile(viewModel.recordingModel.filePath) } + binding.setSeekBarListener(seekBarListener) + val model = sharedViewModel.playingRecording if (model != null) { Log.i("$TAG Loading recording [${model.fileName}] from shared view model") diff --git a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt index 2959a87646..36e7916c71 100644 --- a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt @@ -175,6 +175,14 @@ class RecordingMediaPlayerViewModel } } + @UiThread + fun seekTo(position: Int) { + coreContext.postOnCoreThread { + seekPlaybackTo(position) + startPlayback() + } + } + @WorkerThread private fun startPlayback() { if (!::player.isInitialized) return @@ -232,6 +240,18 @@ class RecordingMediaPlayerViewModel updatePositionJob = null } + @WorkerThread + private fun seekPlaybackTo(position: Int) { + if (!::player.isInitialized) return + + if (player.state == Player.State.Closed) { + player.open(recordingModel.filePath) + } + + Log.i("$TAG Seeking player to position [$position]") + player.seek(position) + } + @WorkerThread private fun stop() { if (!::player.isInitialized) return diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index d8adb9ef11..24134db9ba 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -32,6 +32,7 @@ import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.widget.EditText import android.widget.ImageView +import android.widget.SeekBar import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.annotation.DimenRes @@ -633,6 +634,11 @@ fun setFlexboxLayoutWrapBefore(view: View, wrap: Boolean = false) { view.layoutParams = params } +@BindingAdapter("seekBarListener") +fun setSeekBarListener(seekBar: SeekBar, listener: SeekBar.OnSeekBarChangeListener) { + seekBar.setOnSeekBarChangeListener(listener) +} + @BindingAdapter("emojiPickedListener") fun EmojiPickerView.setEmojiPickedListener(listener: EmojiPickedListener) { setOnEmojiPickedListener { emoji -> diff --git a/app/src/main/res/drawable/media_player_seekbar.xml b/app/src/main/res/drawable/media_player_seekbar.xml new file mode 100644 index 0000000000..345aae3e19 --- /dev/null +++ b/app/src/main/res/drawable/media_player_seekbar.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/media_player_seekbar_thumb.xml b/app/src/main/res/drawable/media_player_seekbar_thumb.xml new file mode 100644 index 0000000000..22291800fc --- /dev/null +++ b/app/src/main/res/drawable/media_player_seekbar_thumb.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/file_media_viewer_child_fragment.xml b/app/src/main/res/layout/file_media_viewer_child_fragment.xml index d3274339e0..4af9021f6b 100644 --- a/app/src/main/res/layout/file_media_viewer_child_fragment.xml +++ b/app/src/main/res/layout/file_media_viewer_child_fragment.xml @@ -5,9 +5,13 @@ + + @@ -75,24 +79,20 @@ app:layout_constraintStart_toStartOf="parent" app:tint="@color/bc_white"/> - + app:layout_constraintEnd_toStartOf="@id/duration" /> + @@ -14,6 +15,9 @@ + @@ -62,24 +66,20 @@ app:layout_constraintStart_toStartOf="parent" app:tint="@color/bc_white"/> - + app:layout_constraintEnd_toStartOf="@id/duration" /> Date: Fri, 5 Dec 2025 10:33:35 +0100 Subject: [PATCH 366/593] Bumped version code to 6.01.001 and updated CHANGELOG --- CHANGELOG.md | 3 ++ app/build.gradle.kts | 2 +- .../java/org/linphone/utils/ShortcutUtils.kt | 52 ------------------- 3 files changed, 4 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5096dd44eb..40c250c559 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,8 +45,11 @@ Group changes to describe their impact on the project, as follows: - Un-encrypted conversation warning will be more visible for accounts that support end-to-end encrypted conversations - Made numpad buttons larger by changing their shape - All LDAP fields are mandatory now +- Improved how Android shortcuts are created - Permission fragment will only show missing ones - Added more info into StartupListener logs +- Updated password forgotten procedure, will use online account manager platform +- Added back "large heap" to AndroidManifest ### Fixed - No audio focus & wrong audio manager mode when TelecomManager isn't supported by device diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a1a8eb02f3..26cab4f8ce 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -100,7 +100,7 @@ android { applicationId = packageName minSdk = 28 targetSdk = 36 - versionCode = 600020 // 6.00.020 + versionCode = 601001 // 6.01.001 versionName = "6.1.0-alpha" manifestPlaceholders["appAuthRedirectScheme"] = packageName diff --git a/app/src/main/java/org/linphone/utils/ShortcutUtils.kt b/app/src/main/java/org/linphone/utils/ShortcutUtils.kt index 70fcd14ddb..700d082c29 100644 --- a/app/src/main/java/org/linphone/utils/ShortcutUtils.kt +++ b/app/src/main/java/org/linphone/utils/ShortcutUtils.kt @@ -84,58 +84,6 @@ class ShortcutUtils { } } - /* - @WorkerThread - fun createShortcutsToChatRooms(context: Context) { - if (ShortcutManagerCompat.isRateLimitingActive(context)) { - Log.e("$TAG Rate limiting is active, aborting") - return - } - - Log.i("$TAG Creating dynamic shortcuts for conversations") - val defaultAccount = coreContext.core.defaultAccount - if (defaultAccount == null) { - Log.w("$TAG No default account found, skipping...") - return - } - - var count = 0 - for (chatRoom in defaultAccount.chatRooms) { - if (defaultAccount.params.instantMessagingEncryptionMandatory && - !chatRoom.hasCapability(ChatRoom.Capabilities.Encrypted.toInt()) - ) { - Log.w( - "$TAG Account is in secure mode, skipping not encrypted conversation [${LinphoneUtils.getConversationId( - chatRoom - )}]" - ) - continue - } - - if (count >= 4) { - Log.i("$TAG We already created [$count] shortcuts, stopping here") - break - } - - val shortcut: ShortcutInfoCompat? = createChatRoomShortcut(context, chatRoom) - if (shortcut != null) { - Log.i("$TAG Created dynamic shortcut for ${shortcut.shortLabel}") - try { - val keepGoing = ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) - if (keepGoing) { - count += 1 - } else { - break - } - } catch (e: Exception) { - Log.e("$TAG Failed to push dynamic shortcut for ${shortcut.shortLabel}: $e") - } - } - } - Log.i("$TAG Created $count dynamic shortcuts") - } - */ - @WorkerThread fun createDynamicShortcutToChatRoom(context: Context, chatRoom: ChatRoom) { val shortcut: ShortcutInfoCompat? = createChatRoomShortcut(context, chatRoom) From 77b933c5a89e93103e3eef7db0643e6c352ff194 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 5 Dec 2025 10:53:50 +0100 Subject: [PATCH 367/593] Migrated translations from release/6.0 branch --- app/src/main/res/drawable/arrow_green.xml | 21 - app/src/main/res/drawable/arrow_red.xml | 21 - app/src/main/res/values-ca/strings.xml | 9 + app/src/main/res/values-cs/strings.xml | 851 ++++++++++++++++++++ app/src/main/res/values-de/strings.xml | 844 ++++++++++++++++++++ app/src/main/res/values-es/strings.xml | 309 ++++++++ app/src/main/res/values-eu/strings.xml | 25 + app/src/main/res/values-fi/strings.xml | 5 + app/src/main/res/values-fr/strings.xml | 6 - app/src/main/res/values-hu/strings.xml | 160 ++++ app/src/main/res/values-nl/strings.xml | 832 ++++++++++++++++++++ app/src/main/res/values-pt-rBR/strings.xml | 851 ++++++++++++++++++++ app/src/main/res/values-ru/strings.xml | 856 +++++++++++++++++++++ app/src/main/res/values-sk/strings.xml | 848 ++++++++++++++++++++ app/src/main/res/values-uk/strings.xml | 856 +++++++++++++++++++++ app/src/main/res/values-zh-rCN/strings.xml | 833 ++++++++++++++++++++ app/src/main/res/values/dimen.xml | 1 - app/src/main/res/values/strings.xml | 6 - app/src/main/res/xml/locales_config.xml | 10 + 19 files changed, 7289 insertions(+), 55 deletions(-) delete mode 100644 app/src/main/res/drawable/arrow_green.xml delete mode 100644 app/src/main/res/drawable/arrow_red.xml create mode 100644 app/src/main/res/values-ca/strings.xml create mode 100644 app/src/main/res/values-cs/strings.xml create mode 100644 app/src/main/res/values-de/strings.xml create mode 100644 app/src/main/res/values-es/strings.xml create mode 100644 app/src/main/res/values-eu/strings.xml create mode 100644 app/src/main/res/values-fi/strings.xml create mode 100644 app/src/main/res/values-hu/strings.xml create mode 100644 app/src/main/res/values-nl/strings.xml create mode 100644 app/src/main/res/values-pt-rBR/strings.xml create mode 100644 app/src/main/res/values-ru/strings.xml create mode 100644 app/src/main/res/values-sk/strings.xml create mode 100644 app/src/main/res/values-uk/strings.xml create mode 100644 app/src/main/res/values-zh-rCN/strings.xml diff --git a/app/src/main/res/drawable/arrow_green.xml b/app/src/main/res/drawable/arrow_green.xml deleted file mode 100644 index 27f31c09c8..0000000000 --- a/app/src/main/res/drawable/arrow_green.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/drawable/arrow_red.xml b/app/src/main/res/drawable/arrow_red.xml deleted file mode 100644 index 6fd056270e..0000000000 --- a/app/src/main/res/drawable/arrow_red.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml new file mode 100644 index 0000000000..ffa3a0c933 --- /dev/null +++ b/app/src/main/res/values-ca/strings.xml @@ -0,0 +1,9 @@ + + + Ahir + + %s dia + %s dies + %s dies + + diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml new file mode 100644 index 0000000000..4833166668 --- /dev/null +++ b/app/src/main/res/values-cs/strings.xml @@ -0,0 +1,851 @@ + + +]> + + SIP adresa + Zobrazené jméno + + %s den + %s dny + %s dní + + Oznámení o aktivních hovorech &appName; + Služba bude běžet trvale, aby zajistila funkčnost aplikace a příjem hovorů a zpráv bez použití push oznámení. + + %s soubor se nahrává + %s soubory se nahrávají + %s souborů se nahrává + + Neplatný QR kód! + SIP adrese neobsahuje uživatelské jméno! + Příspět do překladu &appName; + Zapisovat logy do Logcatu + Po aktivaci bude nutné aplikaci restartovat.\nPoté budou všechna data aplikace šifrována a přístupná pouze přes aplikaci.\n\nBuďte opatrní, tento krok je nevratný! + 1 týden + Nová skupinová konverzace + Chystáte se volat na zařízení %2$s uživatele %1$s.\nPřejete si hovor opravdu zahájit? + Náhlavní souprava + Šifrovací algoritmus: %s + Uživatel nebyl nalezen + Neplatná SIP adresa, nelze přidat do konference + Instalovat + Vaše komunikace je bezpečná díky našemu koncovému šifrování. + Došlo k chybě při pokusu o stažení a použití vzdálené konfigurace + Některé funkce jako skupinové zprávy nebo videokonference vyžadují účet &appName;\n\nTyto funkce jsou skryté, pokud se zaregistrujete pomocí SIP účtu třetí strany.\n\nPro aktivaci v komerčním projektu nás prosím kontaktujte. + Pro plné využívání &appName; potřebujeme, abyste nám udělili následující oprávnění: + Nahrávání zvuku: Aby Vás váš protějšek slyšel a pro nahrávání hlasových zpráv. + Verze + Kontrola aktualizací + Sdílet logy + Ladící logy byly vyčištěny + Aktivace modulu šifrování selhala! + Díky technologii koncového šifrování v &appName; je zaručena důvěrnost zpráv, hovorů a schůzek. Nikdo nedokáže dešifrovat přenesená data, dokonce ani my. + Odpovědět + Žádné výsledky… + + %s oznámení pro další účet/účty + %s oznámení pro další účet/účty + %s oznámení pro další účet/účty + + Oprávnění k zobrazení příchozích hovorů nebylo uděleno! + Klepnutím zobrazit více informací + Přijme videohovor + Skryje možnosti filtrování + Účastník je ztlumen + Klepnutím zobrazit stav doručení + Zavřít seznam účastníků + Klepnutím upravit název konverzace + Upravit tuto schůzku + Připojit se do konference + Jít na konverzaci + Push notifikace se na vašem zařízení nezdají být dostupné, ale jsou nezbytné pro obnovení účtu s telefonním číslem v mobilní aplikaci. + Chyba při exportování souboru do systémové galerie + Chyba při pokusu o vytvoření přehrávače médií + Odesílání notifikací: Abyste byli informováni, když obdržíte zprávu nebo hovor. + Vytvoření konverzace se nezdařilo! + Mizející zprávy byly povoleny + Nenalezena žádná aplikace k otevření tohoto typu souboru + Soubor nelze otevřít! + Udělit práva administrátora + Z historie budou odebrány všechny zprávy + Zahájit skupinový hovor? + Přidal(a) jste se ke skupině + Úroveň bezpečnosti byla snížena kvůli %s + Maximální počet účastníků byl překročen o %s + Klepnutím přidat další účastníky + Smazat schůzku? + Přejete si smazat schůzku? + Je vyžadováno ověření + Ztracené pakety: %s + SAS algoritmus: %s + Sdílet pozvánku + + Účastník (%s) + Účastníci (%s) + Účastníků (%s) + + Přidat do kontaktů + Smazat + Schůzka byla aktualizována + Klepnuitím odebrat účastníka + Přepíná kameru (přední/zadní) + Přejít na první nepřečtenou zprávu nebo na konec + Zastaví nahrávání hlasové zprávy + Pro vaši bezpečnost musíme ověřit zařízení vašeho protějšku.\nProsím, vyměňte si své kódy: + Vybrat vyzváněcí tón + Označit konverzaci jako přečtenou při zavření oznámení o zprávě + Vyplňte prosím alespoň zobrazované jméno a URL adresu serveru + Hostitel + Režim + Spustit při startu zařízení + Nahrávat videohovory pomocí H265/AV1 + Přijímat zvuk před spojením hovoru (early media - např. hlášky nebo tóny) + &appName; nastavení Android + Vyžadováno ověření + Jméno + Upravit + Ahoj, přidej se ke mně na &appName;! Aplikaci si můžeš zdarma stáhnout zde: %s + Zpráva byla smazána + Informace o hromadném odeslání.\nZjistit více + Vyplňte prosím název a vyberte alespoň jednoho účastníka + Příchozí videohovor pro %s + Upravit adresář CardDAV + Autentizační doména + Zpoždění mezi dvěma dotazy (v milisekundách) + Používat pouze sítě Wi-Fi + Prodleva v milisekundách + Připojení účtu selhalo, zkontrolujte nastavení. + Pokud si přejete nevratně smazat svůj účet, navštivte: https://sip.linphone.org + URI konfiguračního serveru konference + Použít CPIM v \"základních\" konverzacích + Při formátování telefonních čísel nahradit \"+\" za \"00\" + Zařízení nebylo nalezeno… + nové zařízení pro %s + Mizející zprávy byly zakázány + pozvánka na schůzku: + Připojení proběhne za malý moment + Nahrát + Čeká se na šifrování… + Propojit všechny hovory do konference? + Video přiloženo ke zprávě + Účet byl zakázán, nebudete moci příjímat hovory ani zprávy. + Účet se připojuje, prosím počkejte… + Připojení se nezdařilo, protože chybí nebo je neplatné ověření účtu\n%s.\n\nMůžete znovu zadat heslo nebo zkontrolovat nastavení účtu v konfiguraci. + Z historie budou odstraněny všechny hovory + Nastavení mizejících zpráv + Žádný shodný výsledek… + Nenalezena žádná média… + ID zařízení + Doména + Uživatelské jméno + ID pro ověření (je-li odlišné) + Heslo + Telefonní číslo + nebo + Další + Začít + Dnes + Včera + + %s vybrán + %s vybrány + %s vybráno + + Odmítnout + Přijmout + Zrušit + Pokračovat + Volat + Smazat + Tento dialog již nezobrazovat + Ne + Ano + Odstranit + Potvrdit + Oznámení o příchozích hovorech &appName; + Oznámení o zmeškaných hovorech &appName; + Oznámení služby &appName; + Oznámení o zprávách (&appName;) + Reakce uživatele %1$s na: %2$s + Označit jako přečtené + Odpovědět + Zmeškaný hovor od %s + Zmeškaný skupinový hovor od: %s + %s zmeškaných hovorů + Zmeškaný hovor + &appName; + Hledání nových zpráv + &appName; + Probíhá přenos souboru/souborů + + %s soubor se stahuje + %s soubory se stahují + %s souborů se stahuje + + Klepněte pro otevření + Vítejte + v &appName; + Bezpečná, otevřená a Francouzská komunikační aplikace. + Zabezpečená + Otevřená + Bezplatná a otevřená aplikace od roku 2001. + SIP adresa zkopírována do schránky + Nový účet byl nastaven + Soubor byl exportován do systémové galerie + Soubor byl exportován do dokumentů + Chyba při exportu souboru do dokumentů + Hlasitost médií je nízká, nemusíte nic slyšet! + Konfigurace byla úspěšně použita + Obecné podmínky a zásady ochrany soukromí + obecné podmínky + zásady ochrany soukromí + Pokračováním přijímáte naše %1$s a %2$s. + Potvrdit telefonní číslo + Jste si jistý(á), že vaše telefonní číslo je %s? + Přihlásit se + Naskenovat QR kód + Použít SIP účet třetí strany + SIP účet třetí strany + Jednotné přihlášení (SSO) + Neplatná SIP adresa! + Účet již existuje + Ještě nemáte účet? + Registrace + Potvrďte své telefonní číslo + Nesprávné číslo? + Vytvořit + Vytvořte účet pomocí své e-mailové adresy na: + Máte již účet? + Transport + Preferuji vytvoření &appName; účtu + Rozumím + Push notifikace s autentifikačním tokenem nebyla přijata během 5 sekund, zkuste to prosím znovu později + Nastala neočekávaná chyba, zkuste to prosím znovu později + Nesprávné uživatelské jméno nebo heslo + Přihlášení se nezdařilo: chybový kód je %s + Udělit oprávnění + OK + Provést později + Čtení kontaktů: Pro zobrazení vašich kontaktů a zjištění, kdo používá &appName;. + Přístup ke kameře: Pro zachycení videa během videohovorů a konferencí. + Zapomenuté heslo + Zvolte způsob obnovy svého účtu. + Váš účet byl vytvořen pomocí: + Emailu + Telefonního čísla + Kontakty + Hovory + Schůzky + Spravovat profil + Připojeno + Znovu načítám + Zakázáno + Připojování… + Chyba + Zatím není nastaven žádný účet + Přidat účet + Nápověda + O aplikaci &appName; + Zásady ochrany soukromí + Jaké informace &appName; sbírá a používá + Konverzace + Pokročilé + Vaše verze je aktuální + Během kontroly aktualizací nastala chyba + Je dostupná nová aktualizace + Nová verze %s je dostupná. Přejete si aktualizovat? + Ukončit aplikaci + Řešení potíží + Verze aplikace + Verze SDK + Vyčistit logy + ID Firebase projektu + Sdílet odkaz na ladící logy pomocí… + Chyba při nahrávání ladících logů + Zobrazit konfiguraci + Nastavení + Zabezpečení + Šifrovat vše + Varování: po zapnutí již nelze zrušit! + Modul šifrování aktivován + Ppravdu si přejete šifrovat vše? + Zabránit nahrávání rozhraní aplikace + Hovory + Použít softwarové potlačení ozvěny + Zabraňuje tomu, aby ozvěna byla slyšena na vzdálené straně, pokud není k dispozici hardwarové potlačení ozvěny + Kalibrovat potlačení ozvěny + probíhá + žádné echo + %s ms + selhalo + Adaptivní datový tok + Povolit video + Povolit FEC pro video + Vibrovat při příchozím hovoru + Automaticky spouštět nahrávání hovorů + Změnit vyzváněcí tón + Automaticky stahovat soubory + Zpřístupnit stažená média pro jiné aplikace + Kontakty + Přidat LDAP server + Upravit LDAP server + Přidat adresář CardDAV + Konverzace + Zobrazené jméno + URL adresa serveru + Uživatelské jméno + Heslo + Ukládat nové kontakty sem + Synchronizace byla úspěšná + Sychronizace selhala! + Účet CardDAV byl odstraněn + URL adresu serveru (nesmí být prázdná) + Bind DN (identifikátor pro připojení) + Heslo + Použít TLS + Výchozí místo hledání (nesmí být prázdné) + Filtr + Maximální počet výsledků + Časový limit (v sekundách) + Minimální počet znaků pro započetí dotazu + Atributy názvu + Atributy SIP + Doména SIP + Nastala chyba, server LDAP nebyl uložen! + Schůzky + Výchozí rozložení + Aktivní mluvčí + Mozaika + Síť + Povolit IPv6 + Uživatelské rozhraní + Automaticky otevřít číselník + Motiv + Tmavý motiv + Světlý motiv + Automaticky + Hlavní barva + Tunel + Port + Použít dva servery + Druhý hostitel + Druhý port + Zakázáno + Vždy + Automaticky + Pokročilá nastavení + Udržet aplikaci aktivní pomocí služby + ID zařízení + Pouze alfanumerické znaky + URL adresa server pro sdílení souborů + Bude použit proprietární formát souboru + Šifrování médií + Povinné šifrování médií + Vytvořit koncově šifrované schůzky a skupinové hovory + Vyzvánět při příchozím hovoru (early media) + Přenášet zvuk při odchozím hovoru (early media) + Automaticky přijímat příchozí hovory + Prodleva před automatickým přijetím hovoru + URL pro vzdálené nastavení + Stáhnout a použít + Zvuková zařízení + Výchozí vstupní zvukové zařízení + Výchozí výstupní zvukové zařízení + Zvukové kodeky + mono + stereo + Video kodeky + Vývojářská nastavení + Zobrazit vývojářská nastavení + Vývojářská nastavení povolena + Vývojářská nastavení jsou již povolena + Spravovat účet + Podrobnosti + Zařízení + Přidat obrázek + Upravit obrázek + Odebrat obrázek + Tento účet je online, každý Vám může zavolat. + Mezinárodní prefix + Nastavení účtu + Odhlásit se + Vybrat režim účtu + Použít + Režim s koncovým šifrováním + Režim kompatibility + Tento režim Vám umožňuje využívat všechny funkce &appName; a zároveň zůstat kompatibilní s jakoukoli SIP službou díky šifrování typu point-to-point. + Odstranit + Poslední připojení: + Odhlásit se z účtu? + Nastavení účtu + Povolit push notifikace + Push notifikace nejsou dostupné! + Šifrování zpráv je povinné + URL adresa SIP proxy serveru + Odchozí proxy server + Nastavení zásad NAT + URL adresa serveru STUN/TURN + Povolit ICE + Povolit TURN + TURN uživatelské jméno + TURN heslo + AVPF + Platnost (v sekundách) + URI konfiguračního serveru pro audio/video konferenci + Adresa URL serveru CCMP + URL adresa serveru pro klíče koncového šifrování + Režim seskupení + URI hlasové schránky + URI serveru MWI (Message Waiting Indicator) + Formátovat telefonní čísla s mezinárodní předvolbou + Aktualizovat heslo + Vyberte svou zemi, aby &appName; mohl správně spárovat vaše kontakty. + Heslo + Odpovídající účet nebyl nalezen! + Nový hovor + Hledat kontakt nebo historii hovorů + Vytvořit skupinový hovor + Momentálně žádný návrh ani kontakt… + Momentálně žádný hovor… + Konverzace + Opravdu si přejete smazat veškerou historii hovorů? + Opravdu si přejete smazat historii s touto osobou? + Z historie bodou odstraněny všechny hovory + Momentálně žádný kontakt… + Oblíbené + Všechny kontakty + Zobrazit vše + Zobrazit &appName; kontakty + Zobrazit SIP kontakty + Nový kontakt + Upravit kontakt + Příjmení + Společnost + Pracovní pozice + Změny byly úpěšně uloženy + Uložení změn se nezdařilo! + Vytvoření kontaktu se nezdařilo! + Zahodit změny? + Všechny změny budou ztraceny + Telefonní čísla a SIP adresy + Společnost: + Pracovní pozice: + Vztah + Zařízení nenalezeno… + Počet důvěryhodných zařízení: + Další akce + Nastavit předmět skupinového hovoru + Předmět skupinového hovoru + Přidat do oblíbených + Odebrat z oblíbených + Sdílet + Smazat + Kontakt byl odstraněn + Číslo bylo zkopírováno do schránky + Zvýšit úroveň důvěry + Úroveň důvěry + Smazat %s? + Kontakt bude nadobro nevratně smazán. + Vyberte číslo nebo SIP adresu + Pryč + Dostupný včera v %s + Dostupný dnes v %s + Dostupný na %s + Online + Nerušit + Volat + Zpráva + Videohovor + Ověřit + Nepojmenované zařízení + Momentálně žádné konverzace… + Probíhá odstraňování… + %s: + + %s soubor čeká na sdílení + %s soubory čekají na sdílení + %s souborů čeká na sdílení + + Text čeká na sdílení + Označit jako přečtené + Ztlumit + Zrušit ztlumení + Volat + Smazat konverzaci + Opustit skupinu + Mizející zprávy + Nové zprávy budou automaticky smazány, jakmile je všichni přečtou.\nVyberte dobu trvání: + Zakázáno + 1 minuta + 1 hodina + 1 den + 3 dny + Nová konverzace + Pojmenujte prosím konverzaci + Hledat kontakt + Vytvořit skupinovou konverzaci + Momentálně žádný kontakt ani návrh… + Napište něco… + + %s píše… + %s píší… + %s píše… + + Přidat účastníky + Odpovídá na: + Hledat + Informace o konverzaci + Mizející zprávy + Média + Dokumenty + Nenalezeny žádné dokumenty… + Koncově šifrovaná konverzace + Zaručená důvěrnost + Tato konverzace není šifrována! + Dosažen maximální možný počet příloh! + Otevřít nebo exportovat soubor? + Otevřít soubor + Exportovat soubor + Otevřít jako textový soubor? + Nenalezena žádná aplikace pro otevření tohoto typu souboru.\n\nPřejte si jej otevřít jako textový soubor? + Otevřít jako textový soubor + Nahrávka nemůže být přehrána! + Nastavit předmět konverzace + Upravit předmět konverzace + Předmět je povinný + Předmět konverzace + Tato konverzace byla pro vaše bezpečí zakázána. + Nelze vytvořit konverzaci s účastníkem mimo stejnou doménu kvůli bezpečnostním omezením! + Vybraná média nebyla nalezena + Mizející zprávy byly zakázány + Doba platnosti mizejících zpráv byla změněna + Dosažena maximální možná délka trvání + Přidání účastníka(ů) do konverzace se nezdařilo + Konverzace byla úspěšně smazána + Opustil(a) jste skupinu + Konverzace nebyla nalezena + Nenalezeny žádné odpovídající výsledky + Byl dosažen poslední odpovídající výsledek + Pořídit snímek + Otevřít galerii + Vybrat soubor + Členové skupiny (%s) + Přidat účastníky + Administrátor + Smazat historii + Odebrat ze skupiny + Odebrat práva administrátora + Zobrazit profil kontaktu + Přidat do kontaktů + Opravdu si přejete smazat všechny zprávy? + Historie byla úspěšně smazána + %s se připojil do konverzace + %s opustil konverzaci + %s je nyní administrátor + %s již není administrátor + Kontakt nebyl nalezen + Nenalezena žádná adresa k uložení do kontaktu + Bude voláno všem účastníkům. + Předmět konverzace byl změněn + Opustil(a) jste skupinu + %s se připojil + %s se odpojil + zařízení pro %s bylo odstraněno + %s je administrátor + %s již není administrátor + Mizející zprávy byly povoleny + Doba platnosti mizejících zpráv je nyní %s + Klíč identity LIME byl změněn pro %s + Detekován útok typu MIM (man-in-the-middle) pro %s + nový předmět: %s + Média a dokumenty + Sdílená média + Sdílené dokumenty + Přeposlat zprávu… + Zpráva byla přeposlána + Přeposlání zprávy bylo zrušeno + Přečteno %s + Doručeno %s + Odesláno %s + Chyba %s + Reakce %s + %1$s %2$s + Klepnutím odstranit + Přeposláno + schůzka aktualizována: + schůzka zrušena: + hlasová zpráva + Dnes není naplánována žádná schůzka + Nová schůzka + Schůzka + Hromadné odeslání + Zvolte konec + Zvolte čas začátku + Zvolte datum začátku + Časová zóna + Jednorázová + Přidat popis + Přidat účastníky + Přidat mluvčího + Odeslat pozvánku účastníkům + Připojit se ke schůzce ihned + Organizátor + Vytvořit událost v kalendáři + Schůzka byla smazána + Schůzka nebyla nalezena! + Popis + Upravit schůzku + Zrušit schůzku + Přidat název… + Smazat schůzku + Schůzka byla vytvořena + Schůzka byla aktualizována + Schůzka byla zrušena + Schůzka zrušena + Naplánování schůzky se nezdařilo! + Odeslání všech pozvánek na schůzku selhalo! + Odeslání pozvánek na schůzku některým účastníkům selhalo! + Adresa schůzky zkopírována do schránky + Připojit se + Zrušit + Probíhá připojování + Připojení ke schůzce se nezdařilo! + Odchozí hovor + Příchozí hovor + Příchozí videohovor + Ukončil(a) jste hovor + Protistrana ukončila hovor + Příchozí hovor pro %s + Přepojit %s na… + Aktuální hovory + Žádný další hovor + Potvrdit přepojení hovoru + Chystáte se přepojit hovor %1$s na %2$s. + Přepojit + Nový hovor + Seznam hovorů + Číselník + Zprávy + Pauza + Obnovit + Zavěsit + Rozložení + Probíhá + Vyzvání + Příchozí + Aktivní + Pozastaveno + Pozastaveno vzdálenou stranou + Obnovuji… + Ukončeno + Koncově šifrováno pomocí ZRTP + Znovu ověřit ZRTP SAS + Šifrováno point-to-point pomocí SRTP + Hovor není šifrován + Seznam hovorů + Hovor se nahrává + %s nahrává + %s hovorů + %s pozastavené hovory + Vytvořit konferenci + Oprávnění pro přístup ke kameře bylo zamítnuto! + Oprávnění k nahrávání zvuku bylo zamítnuto! + Ověřit zařízení + Pro vaši bezpečnost musíme znovu ověřit zařízení vašeho protějšku.\nProsím, vyměňte si znovu své kódy: + Váš kód: + Kód protějšku: + Žádná shoda + Bezpečnostní upozornění + Zkusit znovu + Důvěrnost tohoto hovoru může být narušena! + Sluchátko + Reproduktor + Bluetooth (%s) + Naslouchadlo (%s) + Sluchátka + Zvuk + Kodek: %s + Šířka pásma: %s + Míra ztrát: %s + Buffer pro jitter: %s + Video + Rozlišení: %s + FPS: %s + FEC + Opravené pakety: %s + Šířka pásma: %s + Šifrování médií + Šifrování médií: %s + Post-kvantové ZRTP + Algoritmus pro dohodu o klíči: %s + Hashovací algoritmus: %s + Autentizační algoritmus: %s + Historie byla smazána + Zařízení ověřeno + Hovor se přepojuje + Hovor byl úspěšně přepojen + Přepojení hovoru selhalo! + Uživatel je zaneprázdněn + Nekompatibilní paramentry médií + Služba není dostupná nebo nastala chyba síťě + Časový limit serveru + Dočasně nedostupné + Čekání na další účastníky… + Sdílet obrazovku + Účastníci + Vytvoření skupinového hovoru selhalo! + Propojení hovorů selhalo! + Odstranit %s z konference? + Opravdu si tohoto účastníka přejete odebrat z konference? + Účastník byl odebrán z konference + Připojování… + Pozastaveno + sdílí svou obrazovku + Mozaika + Mluvčí + Pouze zvuk + Příliš mnoho účastníků pro mozaikové rozložení + Vzdálený skupinový hovor + Místní skupinový hovor + Nahrávky + Momentálně žádné nahrávky… + Oblíbené + Zatím žádné oblíbené kontakty + Zobrazit kontakt + Kopírovat adresu SIP + Kopírovat telefonní číslo + Smazat historii + Pozvat + Znovu odeslat + Stav doručení + Přeposlat + Kopírovat + Stáhnout + Sdílet + Oranžová + Žlutá + Zelená + Modrá + Červená + Růžová + Fialová + Zde se zobrazí vybraní účastnící + Chyba při připojení účtu(ů) + Zvolený účet je momentálně zákázán + Nejste připojen(a) k internetu + Mód \"pouze Wi-Fi\" byl povolen + Operace probíhá, prosím počkejte + Konverzace + Kontakty + Oblíbené + Návrhy + Oprávnění k zasílání oznámení nebylo uděleno! + + %s nová hlasová zpráva + %s nové hlasové zprávy + %s nových hlasových zpráv + + Přeskočit + Zapomenuté heslo? + Přeskočit + Schůzka byla zrušena + Kontakt je důvěryhodný + Kontakt není důvěryhodný! + Kontakt je dostupný + Kontakt je nedostupný + Otevřít menu + Jít zpět + Zavřít oznámení + Uložit změny + Zobrazit menu + Potvrdit nový seznam účastníků + Přepíná viditelnost hesla + Rozbalí/sbalí spodní panel + Ukončí hovor + Přijme hovor + Spustí hovor + Spustí videohovor + Povolí/zakáže odesílání obrazu z kamery + Vypne/zapne mikrofon + Změní výstupní zvukové zařízení + Hovor je pozastaven + Zobrazí statistiky hovoru + Tento hovor je nahráván + Odebere poslední číslici nebo znak + Spojí hovory do konference + Zobrazí možnosti filtrování + Vyčistí aktuální filtr + Vytvoří skupinovou konverzaci + Spustí skupinový hovor + Zobrazí číselník + Klepněte pro zobrazení všech voleb + Účastník hovoří + Přidat účastníky + Spustí/zastaví přehrávání zvuku + Spustí/zastaví přehrávání videa + Sdílet soubor + Uložit soubor + Obrázek přiložen ke zprávě + Soubor přiložen ke zprávě + Tato zpráva je odpověďí na předchozí zprávu + Tato zpráva byla přeposlána z jiné konverzace + Spustí/zastaví přehrávání hlasové zprávy + Odebrat tento soubor ze seznamu příloh + Zavřít přílohy + Konverzace byla ztlumena + Mizející zprávy jsou povoleny + Zruší nahrávání hlasové zprávy + Spustí nahrávání hlasové zprávy + Odešle zprávu v konverzaci + Zpráva již dále nebude odpověďí na předchozí zprávu + Otevře výběr emoji + Spustí výběr souboru + Zapne/vypne ztlumení konverzace + Konverzace se odstraňuje + Tato konverzace není zabezpečená + Hledat směrem vzhůru + Hledat směrem dolů + Začít novou konverzaci + Přejít na dnešek + Naplánovat schůzku + Naplánovat tuto schůzku + Sdílet adresu schůzky + Seznam účastníků + Zařízení je důvěryhodné + Upravit kontakt + Odebrat pole + Zobrazit filtr seznamu kontaktů + Vytvořit kontakt + Smazat toto CardDAV nastavení + Uložit CardDAV nastavení + Smazat toto LDAP nastavení + Uložit LDAP nastavení + Přehraje záznam hovoru + Kopírovat text do schránky + Hlasové zprávy jsou dostupné + Dlouhým klepnutím vytočte hlasovou schránku + Kontakt byl úspěšně vytvořen + Vyplňte prosím buďto jméno, příjmení nebo název společnosti + Zkontrolujte všechna zařízení kontaktu, abyste měli jistotu, že vaše komunikace bude zabezpečená a nezměněná.\nJakmile budou všechna ověřena, dosáhnete nejvyšší úrovně důvěry. + Zprávy v této konverzaci jsou koncově šifrovány. Dešifrovat je může pouze váš protějšek. + Poslali jsme vám ověřovací kód na vaše telefonní číslo %1$s.\n\nZadejte prosím níže ověřovací kód: + Push notifikace se na vašem zařízení zdají být nedostupné, ale jsou nezbytné pro vytvoření účtu v mobilní aplikaci.\n\nZveme vás proto, abyste si místo toho vytvořili účet na naší webové platformě: + Tento režim zajišťuje důvěrnost vašich dat. Naše technologie koncového šifrování poskytuje nejvyšší úroveň zabezpečení pro vaši komunikaci. + &appName; nemůže soubor otevřít.\n\nPřejete si jej otevřít v jiné aplikaci (pokud to bude možné) nebo jej exportovat do vašeho zařízení? + Úprava schůzky selhala! + Pomozte vývojářům řešit problémy odesláním protokolů do Crashlytics po pádu + Adresa URL serveru pro sdílení logů + Momentálně žádný SIP kontakt… + Změnit filtr + Pomozte zpřístupnit aplikaci co největšímu počtu lidí. + Průvodce &appName; + Naučte se krok za krokem ovládat všechny funkce aplikace. + Předejte své diagnostické protokoly, abyste usnadnili řešení chyb. + Kliknutím zrušíte soubory nebo text čekající na sdílení + Zapnuto + Koncové šifrování pomocí ZRTP + Přihlásit se k odběru informací o přítomnosti + Tento účet je offline, pravděpodobně proto, že právě nejste připojeni k internetu. + Odpojeno + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 0000000000..9b5644ee8b --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,844 @@ + + +]> + + SIP-Adresse + Geräte-ID + Anzeigename + Domäne + Benutzername + Authentifizierungs ID (falls anders) + Passwort + Telefonnummer + oder + Weiter + Start + Heute + Gestern + + %s Tag + %s Tage + + + %s ausgewählt + %s ausgewählt + + + Ablehnen + Akzeptieren + Abbrechen + Fortsetzen + Anruf + Löschen + Installieren + Diesen Dialog nicht mehr anzeigen + Nein + Ja + Entfernen + Bestätigen + &appName; aktive Anrufe Benachrichtigungen + &appName; eingehende Anrufe Benachrichtigungen + &appName; verpasste Anrufe Benachrichtigungen + &appName; Servicebenachrichtigung + Dieser Service wird ständig ausgeführt, um die App am Leben zu erhalten und Ihnen den Empfang von Anrufen und Nachrichten ohne Push-Benachrichtigungen zu ermöglichen. + &appName; Instant Messages Benachrichtigungen + %1$s hat mit %2$s reagiert + Als gelesen markieren + Antworten + Verpasster Anruf von %s + Verpasster Gruppenanruf: %s + %s verpasste Anrufe + Verpasster Anruf + &appName; + Nach neuen Nachrichten suchen + &appName; + Dateiübertragung läuft + + %s Datei wird hochgeladen + %s Dateien werden hochgeladen + + + %s Datei wird heruntergeladen + %s Dateien werden heruntergeladen + + + Zum Öffnen klicken + Willkommen + in &appName; + Eine sichere, Open Source und französische Kommunikations-App. + Gesichert + Ihre Kommunikation ist dank unserer Ende-zu-Ende-Verschlüsselung sicher. + Open source + Eine kostenlose Open-Source-Anwendung seit 2001. + SIP-Adresse in die Zwischenablage kopiert + Neues Konto konfiguriert + Die Datei wurde in die native Galerie exportiert + Fehler beim Versuch, die Datei in die native Galerie zu exportieren + Die Datei wurde in Dokumente exportiert + Fehler beim Versuch, die Datei in Dokumente zu exportieren + Die Medienlautstärke ist niedrig, Sie hören möglicherweise nichts! + Konfiguration erfolgreich angewendet + Fehler beim Versuch, die Remote-Konfiguration herunterzuladen und anzuwenden + Fehler beim Erstellen des Media Players + Allgemeine Geschäftsbedingungen & Datenschutz + allgemeine Bedingungen + Datenschutzrichtlinie + Indem Sie fortfahren, akzeptieren Sie unsere %1$s und %2$s. + Telefonnummer bestätigen + Sind Sie sicher, dass Ihre Rufnummer %s ist? + Login + QR-Code scannen + Ungültiger QR-Code! + Drittanbieter-SIP-Konto verwenden + Drittanbieter-SIP-Konto + Single Sign-on + SIP Adresse ist ungültig! + SIP Adresse enthält keinen Benutzernamen! + Das Konto ist bereits vorhanden + Noch kein Konto? + Registrieren + Bestätigen Sie Ihre Telefonnummer + Wir haben einen Bestätigungscode an Ihre Telefonnummer %1$s gesendet.\n\nBitte geben Sie den Bestätigungscode unten ein: + Falsche Nummer? + Erstellen + Erstellen Sie mit Ihrer E-Mail ein Konto bei: + Haben Sie bereits ein Konto? + Transport + Für einige Funktionen ist ein &appName;-Konto erforderlich, z. B. Gruppennachrichten, Videokonferenzen…\n\nDiese Funktionen sind ausgeblendet, wenn Sie sich mit einem SIP-Konto eines Drittanbieters registrieren.\n\nUm sie in einem kommerziellen Projekt zu aktivieren, kontaktieren Sie uns bitte. + Ich möchte lieber ein &appName;-Konto erstellen + Ich verstehe + Push Benachrichtigung mit Authentifizierungstoken nicht innerhalb von 5 Sekunden empfangen, bitte versuchen Sie es später erneut + Unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut. + Falscher Benutzername oder Passwort + Anmeldung fehlgeschlagen: Fehlercode ist %s + Berechtigungen erteilen + OK + Mach es später + Um &appName; in vollem Umfang genießen zu können, müssen Sie uns die folgenden Berechtigungen erteilen: + Kontakte: Um Ihre Kontakte anzuzeigen und herauszufinden, wer &appName; verwendet. + Benachrichtigungen: Um informiert zu werden, wenn Sie eine Nachricht oder einen Anruf erhalten. + Mikrofon: Damit Ihre Gesprächspartner Sie hören können. + Kamera: Zum Aufnehmen von Videos während Videoanrufen und -konferenzen. + Kontakte + Anrufe + Chats + Besprechungen + Profil verwalten + Verbunden + Aktualisieren + Deaktiviert + Verbinden… + Fehler + Noch kein Konto konfiguriert + Konto hinzufügen + Hilfe + Über &appName; + Datenschutzrichtlinie + Welche Informationen &appName; sammelt und verwendet + Version + Update prüfen + Erweitert + Ihre Version ist aktuell + Beim Suchen nach einem Update ist ein Fehler aufgetreten + Update verfügbar + Eine neue Version %s ist verfügbar. Möchten Sie aktualisieren? + App beenden + Fehlerbehebung + Logs in Logcat drucken + Logs löschen + Logs teilen + App-Version + SDK version + Firebase Project ID + Debug logs Link teilen mit… + Debug Logs gelöscht + Debug Logs Hochladen fehlgeschlagen + Konfiguration anzeigen + Einstellungen + Sicherheit + Alles verschlüsseln + Achtung: Sobald es aktiviert ist, kann es nicht mehr deaktiviert werden! + Nach der Aktivierung müssen Sie die App neu starten.\nDanach werden alle Anwendungsdaten verschlüsselt und sind nur noch über die Anwendung zugänglich.\n\nVorsicht, dieser Vorgang kann nicht rückgängig gemacht werden! + Aufzeichnung der Benutzeroberfläche verhindern + Anrufe + Verwenden Sie die Software Echounterdrückung + Verhindert, dass das Echo am entfernten Ende gehört wird, wenn kein Hardware Echokompensator verfügbar ist. + Echokompensator kalibrieren + im Gange + kein Echo + %s ms + fehlgeschlagen + Adaptive Geschwindigkeitsregelung + Video aktivieren + Video FEC aktivieren + Vibrieren während ein eingehender Anruf klingelt + Automatische Anrufaufzeichnung starten + Klingelton ändern + Chats + Dateien automatisch herunterladen + Heruntergeladene Medien öffentlich machen + Chats beim Schließen der Benachrichtigung als gelesen markieren + Kontakte + LDAP-Server hinzufügen + LDAP-Server bearbeiten + CardDAV-Adressbuch hinzufügen + CardDAV-Adressbuch bearbeiten + Anzeigename + Server-URL + Benutzername + Passwort + Authentifizierungsbereich + Neu erstellte Kontakte speichern + Synchronisierung erfolgreich + Synchronisierungfehler! + CardDAV-Konto entfernt + Mindestens ein Pflichtfeld wurde nicht ausgefüllt + Server-URL (darf nicht leer sein) + DN binden + Passwort + TLS verwenden + Suchbasis (darf nicht leer sein) + Filter + Maximale Ergebnisse + Timeout (in Sekunden) + Verzögerung zwischen zwei Abfragen (in Millisekunden) + Min Zeichen zum Starten einer Abfrage + Namensattribute + SIP-Attribute + SIP-Domäne + Ein Fehler ist aufgetreten, LDAP-Server nicht gespeichert! + Besprechungen + Standardlayout + Aktiver Lautsprecher + Mosaik + Netzwerk + Nur Wi-Fi Netzwerke verwenden + IPv6 erlauben + Benutzeroberfläche + Wähltastatur automatisch öffnen + Theme + Dunkles Thema + Helles Thema + Auto + Hauptfarbe + Tunnel + Host + Port + Zwei Server verwenden + Zweiter Host + Zweiter Port + Modus + Deaktiviert + Immer + Auto + Erweiterte Einstellungen + Beim Booten des Geräts starten + App mit Service lebendig halten + Gerät ID + Verwendet ein proprietäres Dateiformat + Medienverschlüsselung + Medienverschlüsselung erzwingen + Remotebereitstellung URL + Download & anwenden + Audiogeräte + Standard Audioeingabegerät + Standard Audioausgabegerät + Audio-Codecs + Video-Codecs + Entwicklereinstellungen bereits aktiviert + Konto verwalten + Details + Geräte + Kein Gerät gefunden… + Bild hinzufügen + Bild bearbeiten + Bild entfernen + Dieses Konto ist online, jeder kann Sie anrufen. + Das Konto wurde deaktiviert. Sie erhalten keine Anrufe oder Nachrichten. + Das Konto stellt eine Verbindung zum Server her, bitte warten… + Die Kontoverbindung ist fehlgeschlagen. Überprüfen Sie Ihre Einstellungen. + Internationale Vorwahl + Wählen Sie Ihr Land aus, um &appName; die Zuordnung Ihrer Kontakte zuzulassen. + Kontoeinstellungen + Abmelden + Kontomodus auswählen + Anwenden + Ende-zu-Ende verschlüsselter Modus + Interoperabler Modus + Dieser Modus garantiert die Vertraulichkeit Ihrer Daten. Unsere Ende-zu-Ende-Verschlüsselungstechnologie bietet ein Höchstmaß an Sicherheit für Ihre Kommunikation. + In diesem Modus können Sie alle &appName; Funktionen nutzen und gleichzeitig durch Punkt-zu-Punkt Verschlüsselung die Interoperabilität mit jedem SIP-Dienst aufrechterhalten. + Entfernen + Letzte Verbindung: + Von Ihrem Konto abmelden? + Wenn Sie Ihr Konto dauerhaft löschen möchten, gehen Sie zu: https://sip.linphone.org + Kontoeinstellungen + Push Benachrichtigungen zulassen + Push Benachrichtigungen sind nicht verfügbar! + IM Verschlüsselung obligatorisch + SIP proxy Server URL + Ausgehender Proxy + STUN/TURN Server URL + NAT-Richtlinien + ICE aktivieren + TURN-Benutzername + TURN-Passwort + AVPF + Ablauf (in Sekunden) + Konferenzfabrik URI + Audio-Video Konferenzfabrik URI + CCMP Server URL + E2E Verschlüsselungs Schlüssel Servers URL + Bundle Modus + CPIM in \"basic\" Chats verwenden + Voicemail-URI + MWI Server URI (wartende Nachrichten Anzeige) + Beim Formatieren von Telefonnummern + durch 00 ersetzen + Passwort aktualisieren + Authentifizierung erforderlich + Verbindung fehlgeschlagen, da Authentifizierung für Konto %s fehlt oder ungültig ist.\n\nSie können das Passwort erneut eingeben oder Ihre Kontokonfiguration in den Einstellungen überprüfen. + Passwort + Es konnte kein passendes Konto gefunden werden! + Neuer Anruf + Kontakt oder Anrufverlauf durchsuchen + Gruppenanruf erstellen + Kein Vorschlag und im Moment kein Kontakt… + Betreff für Gruppenanruf festlegen + Betreff des Gruppenanrufs + Im Moment kein Anruf… + Chat + Möchten Sie wirklich den gesamten Anrufverlauf löschen? + Alle Anrufe werden aus dem Verlauf gelöscht + Möchten Sie den Verlauf mit dieser Person wirklich löschen? + Alle Anrufe werden aus dem Verlauf gelöscht + Im Moment kein Kontakt… + Favoriten + Alle Kontakte + Alles sehen + &appName; Kontakte + SIP Kontakte + Neuer Kontakt + Kontakt bearbeiten + Vorname + Nachname + Unternehmen + Berufsbezeichnung + Änderungen wurden erfolgreich gespeichert + Änderungen konnten nicht gespeichert werden! + Kontakt erfolgreich erstellt + Kontakt konnte nicht hergestellt werden! + Änderungen nicht speichern? + Alle Änderungen werden verloren + Mindestens ein Pflichtfeld wurde nicht ausgefüllt + Telefonnummern & SIP Adressen + Unternehmen: + Berufsbezeichnung: + Vertrauen + Kein Gerät gefunden… + Anzahl vertrauenswürdiger Geräte: + Andere Aktionen + Bearbeiten + Zu Favoriten hinzufügen + Aus Favoriten entfernen + Teilen + Löschen + Kontakt wurde entfernt + Nummer in die Zwischenablage kopiert + Erhöhen Sie das Vertrauensniveau + Sie sind dabei, das Gerät %2$s von %1$s anzurufen.\nMöchten Sie den Anruf durchführen? + Vertrauensstufe + Überprüfen Sie alle Ihre Kontaktgeräte, um sicherzustellen, dass Ihre Kommunikation geschützt und unverändert bleibt.\nWenn alles verifiziert ist, erreichen Sie das maximale Vertrauensniveau. + %s löschen? + Dieser Kontakt wird endgültig entfernt. + Eine Nummer oder SIP Adresse wählen + Verfügbar + Online um %s + Online Heute um %s + Online Gestern um %s + Abwesend + Nicht stören + Anruf + Nachricht + Videoanruf + Verifizieren + Unbenanntes Gerät + Kein Chat im Moment… + Entfernung läuft… + %s: + + %s Datei wartet auf Freigabe + %s Dateien warten auf Freigabe + + Text wartet darauf, geteilt zu werden + Als gelesen markieren + Stumm schalten + Stummschaltung aufheben + Anruf + Chat löschen + Verlasse die Gruppe + kurzlebiger Nachrichten konfigurieren + Kurzlebiger Nachrichten + Neue Nachrichten werden automatisch gelöscht, sobald sie von allen gelesen wurden.\nWählen Sie eine Dauer: + Deaktiviert + 1 Minute + 1 Stunde + 1 Tag + 3 Tage + 1 Woche + Neuer Chat + Neuer Gruppenchat + Bitte geben Sie einen Namen für den Chat ein + Kontakt suchen + Gruppenchat erstellen + Kein Kontakt und Vorschlag im Moment… + Kein passendes Ergebnis… + Sag etwas… + + %s schreibt… + %s schreiben… + + Teilnehmer hinzufügen + Antwort auf: + Suchen + Chat-Info + Kurzlebiger Nachrichten + Medien + Dokumente + Keine Medien gefunden… + Kein Dokument gefunden… + Ende-zu-Ende verschlüsselter Chat + Nachrichten in diesem Chat sind Ende-zu-Ende verschlüsselt. Nur Ihr Chatpartner kann sie entschlüsseln. + Garantierte Vertraulichkeit + Dank der End-to-End Verschlüsselungstechnologie in &appName; ist die Vertraulichkeit von Nachrichten, Anrufen und Meetings gewährleistet. Niemand kann die ausgetauschten Daten entschlüsseln, nicht einmal wir selbst. + Dieser Chat ist nicht verschlüsselt! + Zu Ihrer Sicherheit wurde diese Konversation deaktiviert. + Maximale Anzahl an Anhängen erreicht! + Chat-Betreff festlegen + Chat-Betreff bearbeiten + Betreff ist obligatorisch + Chat-Betreff + Datei öffnen oder exportieren? + &appName; kann diese Datei nicht öffnen.\n\nMöchten Sie sie in einer anderen App öffnen (falls möglich) oder auf Ihr Gerät exportieren? + Datei öffnen + Datei exportieren + Als reinen Text öffnen + Sprachaufnahme kann nicht abgespielt werden! + Nachricht wurde gelöscht + Konversation konnte nicht erstellt werden! + Aufgrund von Sicherheitsbeschränkungen kann keine Konversation mit einem Teilnehmer erstellt werden, der sich nicht in derselben Domäne befindet! + Ausgewähltes Medium wurde nicht gefunden + Chat-Betreff hat sich geändert + Kurzlebige Nachrichten wurden aktiviert + Kurzlebige Nachrichten wurden deaktiviert + Lebensdauer flüchtiger Nachrichten geändert + Maximale Dauer erreicht + Teilnehmer konnten nicht zur Unterhaltung hinzugefügt werden + Chat wurde erfolgreich gelöscht + Sie haben die Gruppe verlassen + Keine App zum Öffnen dieser Art von Datei gefunden + Chat wurde nicht gefunden + Kein passendes Ergebnis gefunden + Letztes passendes Ergebnis erreicht + Gruppenteilnehmer (%s) + Teilnehmer hinzufügen + Administrator + Verlauf löschen + Aus der Gruppe entfernen + Administratorrechte erteilen + Administratorrechte entfernen + Remove admin rights + Kontaktprofil anzeigen + Möchten Sie wirklich alle Nachrichten löschen? + Alle Nachrichten werden aus dem Verlauf gelöscht + Der Verlauf wurde erfolgreich gelöscht + %s hat sich dem Chat angeschlossen + %s hat den Chat verlassen + %s ist Jetzt Administrator + %s ist nicht mehr Administrator + Kontakt wurde nicht gefunden + Keine Adresse zum Hinzufügen zum Kontakt + Einen Gruppenanruf starten? + Alle Teilnehmer werden angerufen. + Sie sind der Gruppe beigetreten + Sie haben der Gruppe verlassen + %s ist beigetreten + %s hat verlassen + neues Gerät für %s + Gerät für %s entfernt + neues Betreff: %s + %s ist Administrator + %s ist nicht mehr Administrator + Kurzlebige Nachrichten wurden aktiviert + Kurzlebige Nachrichten wurden deaktiviert + Kurzlebige Nachrichten Lebensdauer beträgt jetzt %s + Medien & Dokumente + Geteilte Medien + Geteilte Dokumente + Nachricht weiterleiten an… + Nachricht wurde weitergeleitet + Die Nachrichtenweiterleitung wurde abgebrochen + %s lesen + %s empfangen + %s gesendet + Fehler %s + Reaktionen %s + %1$s %2$s + Zum Entfernen klicken + Weitergeleitet + Besprechungseinladung: + Besprechung aktualisiert: + Treffen abgesagt: + Sprachnachricht + Für heute ist keine Besprechung geplant + Neue Besprechung + Besprechung + Übertragen + Infos zur Übertragen.\nMehr erfahren + Titel hinzufügen… + Startdatum wählen + Startzeit wählen + Endzeit wählen + Zeitzone + Einmal + Beschreibung hinzufügen + Teilnehmer hinzufügen + Klicken Sie hier, um weitere Teilnehmer hinzuzufügen + Sprecher hinzufügen + Einladung an Teilnehmer senden + Jetzt an Besprechung teilnehmen + Veranstalter + Kalenderereignis erstellen + Die Besprechung wurde gelöscht + Das Meeting konnte nicht gefunden werden! + Beschreibung + Besprechung bearbeiten + Besprechung löschen + Besprechung wurde erstellt + Besprechung wurde aktualisiert + Besprechung wurde abgesagt + Besprechung wurde abgesagt + Das festlegen eines Besprechung ist fehlgeschlagen! + Das Bearbeiten der Besprechung ist fehlgeschlagen! + Mindestens ein Pflichtfeld wurde nicht ausgefüllt + Es konnten nicht alle Einladungen zum Besprechung versendet werden! + An einige Besprechung Teilnehmer konnten keine Einladungen gesendet werden! + Besprechungsadresse in die Zwischenablage kopiert + Verbinden + Abbrechen + Verbindung wird hergestellt + Sie werden in Kürze verbunden sein + Beitritt zur Besprechung fehlgeschlagen! + Ausgehender Anruf + Eingehender Anruf + Eingehender Videoanruf + Der Gesprächspartner hat den Anruf beendet + Eingehender Anruf für %s + Eingehender Videoanruf für %s + Weiterleiten %s an… + Aktuelle Anrufe + Kein andere Anruf + Anrufweiterleitung bestätigen + Sie sind dabei, Anruf %1$s an %2$s weiterzuleiten. + Weiterleiten + Neuer Anruf + Anrufliste + Tasten + Nachrichten + Pausieren + Fortsetzen + Aufnahmen + Auflegen + Layout + Im Gange + Klingeln + Eingehend + Aktiv + Pausieren + Fern pausiert + Fortsetzen… + Beendet + Warten auf Verschlüsselung… + Ende-zu-Ende verschlüsselt durch ZRTP + ZRTP SAS erneut validieren + Validierung erforderlich + Punkt-zu-Punkt Verschlüsselung durch SRTP + Anruf ist nicht verschlüsselt + Anrufliste + Anruf wird aufgezeichnet + %s nimmt auf + %s anruft + %s pausierte Anrufe + Alle Anrufe zu einer Konferenz zusammenführen? + Konferenz erstellen + Kamera Erlaubnis abgelehnt! + Überprüfen des Geräts + Zu Ihrer Sicherheit müssen wir Ihr Endgerät authentifizieren.\nBitte tauschen Sie Ihre Codes aus: + Zu Ihrer Sicherheit müssen wir Ihr entsprechendes Gerät erneut authentifizieren.\nBitte tauschen Sie Ihre Codes erneut aus: + Ihr Code: + Entsprechender Code: + Nichts zusammenpasst + Sicherheitswarnung + Versuchen Sie erneut + Das Gesprächsgeheimnis kann gefährdet sein! + Ohrhörer + Lautsprecher + Bluetooth (%s) + Hörgerät (%s) + Headset + Kopfhörer + Audio + Codec: %s + Bandbreite: %s + Verlustrate: %s + Jitter-Puffer: %s + Video + Lösung: %s + FPS: %s + FEC + Verlorene Pakete: %s + Reparierte Pakete: %s + Bandbreite: %s + Medienverschlüsselung + Medienverschlüsselung: %s + Post-Quantum ZRTP + Verschlüsselungsalgorithmus: %s + Schlüsselvereinbarungsalgorithmus: %s + Hash Algorithmus: %s + Authentifizierungsalgorithmus: %s + SAS Algorithmus: %s + Verlauf wurde gelöscht + Gerät validiert + Anruf wird weitergeleitet + Anruf wurde erfolgreich weitergeleitet + Anrufweiterleitung fehlgeschlagen! + Benutzer ist beschäftigt + Benutzer wurde nicht gefunden + Inkompatible Medienparameter + Dienst nicht verfügbar oder Netzwerkfehler + Server-Timeout + Vorübergehend nicht verfügbar + Einladung teilen + Warten auf weitere Teilnehmer… + Bildschirmfreigabe + Teilnehmer + Gruppenanruf konnte nicht erstellt werden! + Anruf konnte nicht zusammengeführt werden! + + %s Teilnehmer + %s Teilnehmer + + %s aus der Konferenz entfernen? + Möchten Sie diesen Teilnehmer wirklich aus der Konferenz entfernen? + Teilnehmer wurde aus der Konferenz geworfen + Beitritt… + Pausiert + gibt seinen Bildschirm frei + Ungültige SIP Adresse, kann nicht zur Konferenz hinzugefügt werden + Mosaik + Lautsprecher + Nur Audio + Zu viele Teilnehmer für Mosaik Layout + Remote Gruppenanruf + Lokaler Gruppenanruf + Aufnahmen + Keine Aufnahme im Moment… + Favoriten + Noch kein Favoritenkontakt + Kein SIP-Kontakt im Moment… + Filter ändern + Zu Kontakte hinzufügen + Siehe Kontakt + SIP-Adresse kopieren + Telefonnummer kopieren + Verlauf löschen + Löschen + Einladen + Erneut senden + Zustelldetails + Antworten + Weiterleiten + Kopieren + Herunterladen + Teilen + Orange + Gelb + Grün + Blau + Rot + Rosa + Lila + Kein Ergebnis gefunden… + Ausgewählte Teilnehmer erscheinen hier + Verbindungsfehler bei den Konten + + %s Benachrichtigung für anderes Konto(en) + %s Benachrichtigungen für anderes Konto(en) + + Das ausgewählte Konto ist derzeit deaktiviert + Sie sind nicht mit dem Internet verbunden + Nur-WLAN-Modus aktiviert + Vorgang wird ausgeführt, bitte warten + Chats + Kontakte + Favoriten + Vorschläge + Berechtigung zum Posten von Benachrichtigungen nicht erteilt! + Berechtigung zum Anzeigen eingehender Anrufe nicht erteilt! + + %s neue Sprachnachricht + %s neue Sprachnachrichten + + Überspringen + Passwort vergessen? + Überspringen + Das Besprechung wurde aktualisiert + Das Besprechung wurde abgesagt + Kontakt ist vertrauenswürdig + Kontakt ist nicht vertrauenswürdig! + Kontakt ist Online + Kontakt ist nicht Online + Schubladenmenü öffnen + Zurück + Benachrichtigung schließen + Änderungen speichern + Menü anzeigen + Neue Teilnehmerliste bestätigen + Klicken um weitere Informationen zu erhalten + Klicken um den Teilnehmer zu entfernen + Schaltet die Passwortsichtbarkeit um + Erweitert/zieht das Bodenblatt ein + Beendet den Anruf + Nimmt den Anruf entgegen + Nimmt den Videoanruf entgegen + Anruf starten + Videoanruf starten + Aktiviert/deaktiviert das Senden Ihres Kamera Feeds + Mikrofon stummschalten/Stummschaltung aufheben + Ändert das Ausgabe Audiogerät + Ändert die verwendete Kamera (vorne/hinten) + Anruf ist angehalten + Zeigt Anrufstatistiken an + Sie zeichnen diesen Anruf auf + Entfernt die letzte Ziffer oder das letzte Zeichen + Führt Anrufe zu einer Konferenz zusammen + Öffnet den Filterbereich + Schließt den Filterbereich + Aktuellen Filter löschen + Erstellt einen Gruppenchat + Gruppenanruf starten + Zeigt den Nummernblock + Klicken Sie hier, um alle verfügbaren Optionen anzuzeigen + Teilnehmer ist stummgeschaltet + Teilnehmer spricht + Teilnehmer hinzufügen + Startet/unterbricht die Audiowiedergabe + Startet/unterbricht die Videoowiedergabe + Datei teilen + Datei speichen + Bild an Nachricht angehängt + Video an Nachricht angehängt + Datei an Nachricht angehängt + Diese Nachricht ist eine Antwort auf eine vorherige Nachricht + Diese Nachricht wurde aus einer anderen Konversation weitergeleitet + Klicken Sie hier, um die Zustelldetails anzuzeigen + Spielt/pausiert die Wiedergabe der Sprachnachricht + Diese Datei aus Anhängen entfernen + Anhänge schließen + Der Chat wurde stummgeschaltet + Kurzlebige Nachrichten sind aktiviert + Scrollt zur ersten ungelesenen Nachricht oder zum Ende + Schließt die Teilnehmerliste + Aufnahme von Sprachnachrichten abbrechen + Aufzeichnung von Sprachnachrichten stoppen + Aufzeichnung von Sprachnachrichten starten + Sendet eine Nachricht im Chat + Die Nachricht ist keine Antwort mehr auf eine vorherige Nachricht + Öffnet den Emoji-Picker + Öffnet den Medien-Picker + Klicken Sie hier, um das Thema dieser Unterhaltung zu bearbeiten + Stummschalten dieses Chats + Der Chat wird entfernt + Dieser Chat ist nicht gesichert + Suche nach oben + Suche nach unten + Neuen Chat starten + Scrollen zu Heute + Eine Besprechung planen + Die Besprechung planen + Die Besprechung bearbeiten + Besprechungsadresse teilen + Teilnehmerliste + Gerät ist vertrauenswürdig + Kontakt bearbeiten + Feld entfernen + Kontaktlistenfilter anzeigen + Einen Kontakt erstellen + Nehmen Sie an der Konferenz teil + Löschen Sie diese CardDAV-Konfiguration + CardDAV Konfiguration speichern + Diese LDAP-Konfiguration löschen + LDAP-Konfiguration speichen + Spielt die Gesprächsaufzeichnung ab + Klingelton auswählen + TURN aktivieren + Frühe Medienübertragung erlauben + Rufsignal bei früher Medienübertragung + Ausgehende frühe Medianübertragung erlauben + Nur alpha-numerische Zeichen + Datei-Upload-URL + Telefonnummern mit internationaler Vorwahl formatieren + Audioaufzeichnung Erlaubnis abgelehnt! + Sie haben den Anruf beendet + Zum Chat + Text in die Zwischenablage kopieren + Sprachnachrichten verfügbar + Lange drücken, um Voicemail anzurufen + Verschlüsselungsmoduls Aktivieren fehlgeschlagen! + Verschlüsselungsmodul aktiviert + Möchten Sie wirklich alles verschlüsseln? + Helfen Sie Entwicklern bei der Fehlerbehebung, indem Sie nach einem Absturz Protokolle an Crashlytics senden + Server-URL für die Protokollfreigabe + Videoanrufe mit H265/AV1 aufzeichnen + Erstellen Sie Ende-zu-Ende-verschlüsselte Meetings und Gruppenanrufe + Automatisches Beantworten eingehender Anrufe + Verzögerung vor der automatischen Anrufannahme + Verzögerung in Millisekunden + Mono + Android-Einstellungen + Entwicklereinstellungen + Entwicklereinstellungen anzeigen + Entwicklereinstellungen aktiviert + Als Klartext öffnen? + Es wurde keine App zum Öffnen dieser Datei gefunden.\n\nMöchten Sie versuchen, die Datei als reinen Text zu öffnen? + Foto aufnehmen + Galerie öffnen + Datei auswählen + Datei kann nicht geöffnet werden! + LIME-Identitätsschlüssel für %s geändert + Man-in-the-Middle-Angriff für %s erkannt + Sicherheitsstufe verringert wegen %s + Maximale Teilnehmerzahl um %s überschritten + Besprechung absagen + Besprechung löschen? + Möchten Sie die Besprechung löschen? + Passwort vergessen + Sie haben Ihren Account erstellt mit: + Benachrichtigungen scheinen auf Ihrem Gerät nicht verfügbar zu sein, sind allerdings nötig um einen Account in der App zu erstellen.\n\nWir laden Sie ein, stattdessen einen Account auf unserer Webplattform zu erstellen: + Einer E-Mail + Einer Telefonnummer + Benachrichtigungen scheinen auf Ihrem Gerät nicht verfügbar zu sein, sind aber für die Wiederherstellung eines Rufnummernkontos in der mobilen App zwingend erforderlich. + &appName; Benutzeranleitung + Lerne alle Funktionen der App zu meistern, Schritt für Schritt. + Hier tippen um Datei- oder Textfreigaben abzubrechen + Zu &appName;-Übersetzungen beitragen + Helfen Sie mit, die App so vielen Menschen wie möglich zugänglich zu machen. + Übermitteln Sie Ihre Diagnoseprotokolle, um die Fehlerbehebung zu erleichtern. + Stereo + Hallo, kommen Sie mit zu &appName;! Sie können es hier %s kostenlos herunterladen + Aktiviert + Wählen Sie aus, wie Sie Ihr Konto wiederherstellen möchten. + Punkt-zu-Punkt Verschlüsselung durch ZRTP + Dieses Konto ist offline, weil Sie vermutlich nicht mit dem Internet verbunden sind. + Getrennt + Anwesendheitsinfos abonnieren + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 0000000000..1d21c16942 --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,309 @@ + + +]> + + Dirección SIP + Identificación de Dispositivo + Nombre para mostrar + Dominio + Nombre de Usuario + Contraseña + o + Número de teléfono + Siguiente + Iniciar + Hoy + Ayer + Denegar + Aceptar + Cancelar + Continuar + Llamar + Eliminar + Instalar + No mostrar este mensaje más + No + Si + Remover + &appName; notificaciones de llamadas activas + &appName; notificaciones de llamadas perdidas + &appName; notificación de servicio + &appName; notificaciones de mensajes instantáneos + Reaccionó %1$s a: %2$s + Marcar como leído + Responder + Llamada perdida de %s + Llamadas de grupo perdidas: %s + %s Llamadas perdidas + Llamada perdida + &appName; + Buscando nuevos mensajes + &appName; + ID de autenticación (si es diferente) + + %s Seleccionado + %s Seleccionados + Seleccionados + + Confirmar + &appName; notificaciones de llamadas entrantes + Este servicio se ejecutará todo el tiempo para mantener activa la aplicación y permitirle recibir llamadas y mensajes sin notificaciones push. + + %s Día + %s Días + Días + + Transferencia de archivo (s) en curso + Clic para abrir + Bienvenido + en &appName; + Una aplicación de comunicación segura, de código abierto y en francés. + Segura + código abierto + Dirección SIP copiada al portapapeles + Nueva cuenta configurada + El archivo se ha exportado a la galería nativa + Error tratando de exportar el archivo a la galería nativa + El archivo ha sido exportado a documentos + El volumen de los medios es bajo, ¡es posible que no escuches nada! + Configuración aplicada satisfactoriamente + Error al intentar descargar y aplicar la configuración remota + Términos generales y política de privacidad + términos generales + política de privacidad + Al continuar, aceptas nuestros %1$s y %2$s. + Confirmar número de teléfono + Acceso + Escanear código QR + Código QR no válido! + Utilice una cuenta SIP de terceros + Cuenta SIP de terceros + Inicio de sesión único + La dirección SIP no es válida! + La cuenta ya existe + Aún no tienes cuenta? + Registrar + Confirma tu número de teléfono + Una aplicación gratuita y de código abierto desde 2001. + Error tratando de exportar archivo a documentos + Error al intentar crear un reproductor multimedia + ¿Estás seguro que tu número de teléfono es %s? + La dirección SIP no contiene un nombre de usuario! + Hemos enviado un código de verificación a tu número de teléfono %1$s.\n\nIntroduce el código de verificación a continuación: + + %s archivo en proceso de carga + %s archivos en proceso de carga + %s archivo en proceso de carga + + Tus comunicaciones están seguras gracias a nuestra encriptación de extremo a extremo. + + %s Archivo en descarga + %s Archivos en descarga + %s Archivo en descarga + + Número equivocado? + Crear + Crea una cuenta con tu correo electrónico en: + Ya tienes una cuenta? + Transporte + Algunas funciones requieren una cuenta &appName; como la mensajería grupal, las videoconferencias, etc.\n\nEstas funciones están ocultas al registrarse con una cuenta SIP de terceros.\n\nPara habilitarlas en un proyecto comercial, contáctenos. + Prefiero crear una cuenta; &appName; + Yo entiendo + Notificación push con token de autenticación no recibida en 5 segundos, inténtelo nuevamente más tarde + Se produjo un error inesperado, inténtelo de nuevo más tarde + Nombre de usuario o contraseña incorrectos + Error al iniciar sesión: el código de error es %s + Conceder permisos + Ok + Para disfrutar plenamente de &appName; necesitamos que nos concedas los siguientes permisos: + Notificaciones de publicaciones: Para ser informado cuando recibes un mensaje o una llamada. + Grabar audio: Para que tu interlocutor pueda escucharte y grabar mensajes de voz. + Contraseña olvidada + Elige cómo recuperar tu cuenta. + Usted creó su cuenta usando: + Un correo electrónico + Un numero de teléfono + Contactos + Llamadas + Conversaciones + Reuniones + Administrar el perfil + Conectado + Refrescando + Deshabilitado + Conectando… + Error + Cuenta no configurada aun + Agregar una cuenta + Ayuda + Acerca de &appName; + Parece que las notificaciones push no están disponibles en tu dispositivo, pero son obligatorias para crear una cuenta en la app móvil.\n\nTe invitamos a crear una cuenta en nuestra plataforma web: + Leer contactos: para mostrar tus contactos y encontrar quién está usando &appName;. + Hazlo más tarde + Acceso a Cámara: Para capturar vídeo durante videollamadas y conferencias. + Las notificaciones push no parecen estar disponibles en su dispositivo, pero son obligatorias para recuperar una cuenta de número de teléfono en la aplicación móvil. + &appName; Guía del usuario + Aprenda a dominar todas las funciones de la aplicación, paso a paso. + Política de privacidad + Qué información recopila y utiliza &appName; + Versión + Comprobar actualización + Ayude a que la aplicación sea accesible para la mayor cantidad de personas posible. + Avanzado + Su versión esta actualizada + Actualización disponible + Hay una nueva versión %s disponible. ¿Quieres actualizarla? + Salir de la aplicación + Solución de problemas + Transmita sus registros de diagnóstico para facilitar la resolución de errores. + Limpiar registros + Compartir registros + Versión de la aplicación + Versión SDK + ID del proyecto de Firebase + Compartir enlaces de registros de depuración usando… + Contribuir a la traducción de &appName; + Se produjo un error al buscar actualizaciones + Imprimir registros en logcat + Se han limpiado los registros de depuración + No se pudieron cargar los registros de depuración + Mostrar configuracion + Ajustes + Seguridad + Cifrar todo + Advertencia: una vez habilitado, ¡no se puede deshabilitar! + ¡Error al habilitar el módulo de cifrado! + Módulo de cifrado habilitado + ¿Realmente quieres cifrarlo todo? + Una vez activada, tendrás que reiniciar la aplicación.\nDespués, todos los datos de la aplicación estarán cifrados y solo se podrá acceder a ellos a través de ella.\n\n¡Cuidado, no se puede deshacer! + Evitar que se grabe la interfaz + Llamadas + Utilizar el cancelador de eco de software + Evita que el eco se escuche en el extremo remoto si no hay un cancelador de eco de hardware disponible + Calibrar el cancelador de eco + en progreso + sin eco + %s ms + fallido + Control de velocidad adaptativo + Habilitar video + Habilitar FEC de vídeo + Vibrar mientras suena la llamada entrante + Iniciar automáticamente la grabación de llamadas + Cambiar tono de llamada + Conversaciones + Descarga automática de archivos + Hacer públicos los medios descargados + Elige un tono de llamada + Marcar la conversación como leída al descartar la notificación del mensaje + Contactos + Agregar servidor LDAP + Editar servidor LDAP + Agregar libreta de direcciones CardDAV + Editar libreta de direcciones CardDAV + Nombre para mostrar + URL del servidor + Nombre de Usuario + Clave + Dominio de autenticación + Almacenar los contactos recién creados + La sincronización fue exitosa + Error de sincronización! + Cuenta CardDAV eliminada + Identificador del dispositivo + Se produjo un error, ¡el servidor LDAP no se guardó! + Respuesta automática de las llamadas entrantes + Interfaz de usuario + Utilice dos servidores + Ayuda a los desarrolladores a solucionar problemas enviando registros a Crashlytics después de un bloqueo + Graba videollamadas usando H265/AV1 + Suena durante las primeras llamadas entrantes + Reuniones + Diseño predeterminado + Altavoz activo + Mosaico + Red + Use solo redes Wi-fi + Permitir IPv6 + Abrir automáticamente el teclado numérico + Tema + Tema Oscuro + Tema claro + Auto + Color principal + Túnel + Anfitrión + Puerto + Segundo anfitrión + Segundo puerto + Modo + Deshabilitado + Siempre + Auto + Configuración avanzada + Iniciar al arrancar el dispositivo + Mantener la aplicación activa mediante el servicio + Solo caracteres alfanuméricos + URL del servidor para compartir archivos + URL del servidor para compartir registros + Utilizará un formato de archivo propio + Cifrado de medios + Cifrado obligatorio de los medios de comunicación + Cree reuniones y llamadas grupales con cifrado de extremo a extremo + Acepta los primeros medios de comunicación + Permitir la salida temprana de medios + Retraso antes de responder automáticamente a una llamada + Retraso en milisegundos + Configuración de desarrollador ya habilitada + Esta cuenta está sin conexión, probablemente porque no estás conectado a Internet en este momento. + Seleccione su país para permitir que &nombre de aplicación; coincida con sus contactos. + Detalles + URL de aprovisionamiento remoto + Descargar y aplicar + Dispositivos de audio + Dispositivo de audio de entrada predeterminado + Dispositivo de salida de audio predeterminado + Códecs de audio + mono + estereo + Códecs de video + &nombre de aplicación; Configuración de Android + Configuración del desarrollador + Mostrar configuración del desarrollador + Configuración de desarrollador habilitada + Administrar cuenta + Dispositivos + No se encontró ningún dispositivo… + Agregar una imagen + Editar imagen + Remover imagen + Esta cuenta está en línea, cualquiera puede llamarte. + La cuenta ha sido desactivada, no recibirás ninguna llamada ni mensaje. + La cuenta se está conectando al servidor, por favor espere… + Error al conectar la cuenta, comprueba tu configuración. + Prefijo internacional + Configuración de la cuenta + Cerrar sesión + Seleccionar modo de cuenta + Aplicar + Modo cifrado de extremo a extremo + Modo interoperable + Mosaico + Clave + Remover + Configuración de la cuenta + Clave + No se encontró ningún dispositivo… + Eliminar + Llamar + Marcar como leído + Llamar + Deshabilitado + Cancelar + Cifrado de medios + Eliminar + Responder + Conversaciones + Contactos + diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml new file mode 100644 index 0000000000..168a6fd7fa --- /dev/null +++ b/app/src/main/res/values-eu/strings.xml @@ -0,0 +1,25 @@ + + + Gailuaren IDa + Erakusteko izena + Domeinua + Erabiltzaile izena + Pasahitza + Telefono zenbakia + edo + Hurrengoa + Gaur + Atzo + + egun %s + %s egun + + + %s aukeratuta + %s aukeratuta + + Ukatu + Onartu + SIP helbidea + Hasi + diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml new file mode 100644 index 0000000000..25e41c7d5d --- /dev/null +++ b/app/src/main/res/values-fi/strings.xml @@ -0,0 +1,5 @@ + + + AVPF + Todennus tarvitaan + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 835d8c85f0..44af1073d1 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -505,10 +505,6 @@ %s est en train d\'écrire… %s sont en train d\'écrire… - - %s est en train d\'enregistrer un message vocal… - %s sont en train d\'enregistrer un message vocal… - Ajouter des participants En réponse à : Chercher @@ -527,8 +523,6 @@ Les messages échangés dans cette conversation peuvent être interceptés et consultés par des personnes autres que le destinataire désiré, la confidentialité n\'est pas garantie ! Cette conversation n\'est pas chiffrée ! Cette conversation a été désactivée pour garantir votre sécurité. - Chiffrement obligatoire - Vous avez activé le chiffrement obligatoire. Vos conversations non chiffrées sont désactivées pour garantir votre sécurité. Vous pouvez recréer cette conversation ou bien désactiver le chiffrement obligatoire dans vos paramètres de compte. Nombre maximum de fichiers atteint ! Nommer la conversation Renommer la conversation diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml new file mode 100644 index 0000000000..364b7c64cc --- /dev/null +++ b/app/src/main/res/values-hu/strings.xml @@ -0,0 +1,160 @@ + + +]> + + Eszköz ID + Domain cím + Felhasználónév + Azonosító ID (ha különbözik) + Jelszó + Telefonszám + vagy + Következő + Indítás + Ma + Tegnap + + %s nap + %s nap + + Elfogad + Mégse + Folytatás + Hívás + Törlés + Telepítés + Ne mutassa többet ezt az ablakot + Nem + Igen + Eltávolít + Megerősít + &appName; bejövő hívások értsítése + &appName; nem fogadott hívások értesítése + &appName; szolgáltató értesítése + &appName; azonnali üzenetek értesítése + Jelölés olvasottnak + Válasz + Nem fogadott hívás %s + Nem fogadott csoporthívás %s + %s nem fogadott hívás + Nem fogadott hívás + &appName; + Fájl(ok) átvitele folyamatban + + %s fájl feltöltve + %s fájl feltöltve + + Érintse meg a megnyitáshoz + Üdvözöljük + a Linphone-nál + Egy biztonságos, nyílt-forráskódú és francia kommunikációs appnál. + Biztonságos + Nyílt forráskódú + Egy ingyenes és nyílt forráskódú alkalmazás 2001 óta. + Új üzenetek fogadása + %1$s Reagált rá: %2$s + Az új fiók konfigurálva + A konfiguráció sikeresen alkalmazva + Hiba történt a médialejátszó készítése közben + Általános feltételek és biztonsági szabályok + általános feltételek + biztonsági szabályok + A folytatással elfogadja az %1$s-et és a %2$s-at. + Erősítse meg a telefonszámot + Bejelentkezés + QR–kód beolvasása + Egyszerű bejelentkezés + Érvénytelen SIP cím! + A SIP cím nem tartalmaz felhasználónevet! + A fiók már létezik + Regisztráció + Rossz telefonszám? + Létrehoz + Hozzon létre egy fiókot az email–címével: + Már van fiókja? + SIP átvitel + Elfogadom + A push értesítés az azonosító tokennel nem érkezett meg 5mp-en belül, próbálja meg később + Váratlan hiba történt, próbálja meg később + Hibás felhasználónév vagy jelszó + Nem sikerült a bejelentkezés, hibakód: %s + Engedélyek biztosítása + OK + Később + Névjegyek: Hozzáférés a névjegyeihez, és láthatja, hogy ki használja a &appName;-t. + Értesítések: Értesítéseket kap a hívásokról és üzenetekről. + Kamera: Videóhívásokhoz és képfelvétel rögzítéséhez. + Elfelejtett jelszó + Hívások + Üzenetek + Meetingek + Profil kezelése + Frissítés + Letiltva + Csatlakozás… + Hiba + Nincs még fiók konfigurálva + Segítség + A Linphone-ról + Milyen adatokat gyűjt és használ a Linphone + Verzió + Haladó + Beállítások + Biztonság + Titkosítás + SIP cím + Elutasít + Ez a szolgáltatás mindig futni fog, hogy ébren tartsa az appot, hogy hívásokat és üzeneteket tudjon fogadni push értesítés nélkül. + SIP cím a vágólapra másolva + Biztos benne, hogy %s a telefonszáma? + Érvénytelen QR–kód! + Úgy tűnik, a push értesítések nem elérhetőek a készülékén, de ezek szükségesek egy fiók létrehozásához a mobil appban.\n\nHozzon létre fiókot a weboldalunkon keresztül: + Fiók hozzáadása + Megjelenített név + + %s fájl letöltve + %s fájl letöltve + + A kommunikációja biztonságban van, köszönhetően a végpontok közötti titkosításunknak. + A hangerő alacsony, lehet, nem hall semmit! + Hiba történt a távoli konfiguráció letöltése és alkalmazása közben + Harmadik féltől származó SIP fiók használata + Harmadik féltől származó SIP fiók + Nincs még fiókja? + Erősítse meg telefonszámát + Elküldtünk egy azonosítási kódot a %1$s telefonszámra.\n\nKérjük adja meg itt az azonosítási kódot: + Névjegyek + Mutassa a konfigurációt + &appName; aktív hívások értesítése + A Linphone teljes élvezetéhez biztosítsa a következő engedélyeket: + Mikrofon: Hogy a hívott fél hallja önt, és hangüzenetek felvételéhez. + Csatlakoztatva + Linphone használati útmutató + + %s kiválasztva + %s kiválasztva + + Hívások + Üzenetek + Névjegyek + Megjelenített név + Felhasználónév + Jelszó + Jelszó + Meetingek + Letiltva + Eszköz ID + Eltávolít + Jelszó + Törlés + Hívás + Jelölés olvasottnak + Hívás + Letiltva + Mégse + Törlés + Válasz + Üzenetek + Névjegyek + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000000..ccdd8db14f --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,832 @@ + + +]> + + Apparaat-ID + Weergavenaam + Gebruikersnaam + Telefoonnummer + of + Volgende + Vandaag + Ontkennen + Accepteren + Domein + Authenticatie-ID (indien anders) + Wachtwoord + + %s dag + %s dagen + + Doorgaan + Installeren + Toon dit dialoogvenster niet meer + Nee + Ja + Weghalen + Bevestigen + &appName; meldingen voor inkomende oproepen + Wissen + Markeer als gelezen + Antwoord + Gemiste oproep van %s + Gemiste groepsoproep: %s + %s gemiste oproepen + Gemiste oproep + &appNaam; + &appName; meldingen voor gemiste oproepen + &appName; servicemelding + &appName; meldingen voor directe berichten + + %s bestand wordt geüpload + %s bestanden worden geüpload + + + %s bestand wordt gedownload + %s bestanden worden gedownload + + Klik om te openen + Welkom + &appNaam; + in &appName; + Beveiligd + Dankzij onze end-to-end-encryptie is uw communicatie veilig. + SIP-adres gekopieerd naar klembord + Nieuw account geconfigureerd + Fout bij het exporteren van een bestand naar de galerij + Bestand is geëxporteerd naar de documenten + Fout bij het exporteren van een bestand naar de documenten + Het mediavolume is laag, u hoort mogelijk niets! + Fout bij het proberen om een mediaspeler te maken + Algemene voorwaarden & privacybeleid + algemene voorwaarden + privacy beleid + Door verder te gaan, accepteert u onze %1$s en %2$s. + Bevestig telefoonnummer + Weet u zeker dat uw telefoonnummer %s is? + Scan de QR-code + SIP-account van derden + Gebruik een SIP-account van derden + Eenmalige aanmelding + SIP-adres is ongeldig! + Account bestaat al + Nog geen account? + Registreren + Bevestig uw telefoonnummer + Verkeerd nummer? + Maak een account aan met uw e-mailadres op: + Maken + Heeft u al een account? + Vervoer + Ik geef er de voorkeur aan om een &appName;-account aan te maken + Ik begrijp + Pushmelding met autorisatietoken niet binnen 5 seconden ontvangen, probeer het later opnieuw + Er is een onverwachte fout opgetreden, probeer het later opnieuw + Verkeerde gebruikersnaam of wachtwoord + Inloggen mislukt: foutcode is %s + Verleen machtigingen + OK + Doe het later + Berichtmeldingen: Om op de hoogte te worden gesteld wanneer u een bericht of een oproep ontvangt. + Toegang tot camera: Om video vast te leggen tijdens videogesprekken en -conferenties. + Wachtwoord vergeten + Kies hoe u uw account wilt herstellen. + U heeft uw account aangemaakt met: + Een e-mail + Contacten + Oproepen + Gesprekken + Vergaderingen + Beheer het profiel + Verbonden + Verversen + Uitgeschakeld + Verbinden… + Nog geen account geconfigureerd + Voeg een account toe + Help + Over &appName; + Privacy beleid + Versie + Controleer op updates + Draag bij aan &appName;-vertaling + Geavanceerd + Update beschikbaar + Er is een nieuwe versie %s beschikbaar. Wilt u updaten? + Afsluiten + Probleemoplossing + Logboeken afdrukken in logcat + Logboeken opschonen + Logs delen + App-versie + SDK-versie + Firebase-project-ID + Debuglogs zijn opgeschoond + Uploaden van debuglogs mislukt + Configuratie weergeven + Instellingen + Beveiliging + Versleutel alles + Inschakelen van het versleutelingsmodule is mislukt! + Versleutelingsmodule ingeschakeld + Weet je zeker dat je alles wilt versleutelen? + Voorkom dat de interface wordt opgenomen + Oproepen + Softwarematige echo-onderdrukking gebruiken + Voorkomt echo aan de andere kant van de lijn als er geen hardwarematige echo-onderdrukking beschikbaar is + Echo-onderdrukking kalibreren + In behandeling + Geen echo + Mislukt + Adaptieve bitrate-regeling + Video inschakelen + Video-FEC inschakelen + Trillen tijdens inkomend gesprek + Oproepen automatisch opnemen + Kies een beltoon + Gesprekken + Bestanden automatisch downloaden + Gedownloade media openbaar maken + Contacten + LDAP-server toevoegen + Weergavenaam + Server-URL + Gebruikersnaam + Wachtwoord + Authenticatiedomein + Synchronisatie is geslaagd + Synchronisatiefout! + CardDAV-account verwijderd + Server-URL (mag niet leeg zijn) + Bind-DN + Wachtwoord + TLS gebruiken + Zoeken + Zoekbasis (mag niet leeg zijn) + SIP-attributen + Er is een fout opgetreden, LDAP-server is niet opgeslagen! + Vergaderingen + Netwerk + Uitgeschakeld + Altijd + Starten bij opstarten van het apparaat + App actief houden via service + Apparaat-ID + Downloaden en toepassen + Audioapparaten + Ontwikkelaarsinstellingen + Ontwikkelaarsinstellingen weergeven + Ontwikkelaarsinstellingen ingeschakeld + Ontwikkelaarsinstellingen zijn al ingeschakeld + Account beheren + Details + Apparaten + Geen apparaat gevonden… + Bewerk foto + Verwijder foto + Dit account is online, iedereen kan je bellen. + Account is verbinding aan het maken met de server, even geduld aub… + Verbinding met account mislukt, controleer je instellingen. + Internationale prefix + Accountinstellingen + Uitloggen + Kies accountmodus + Toepassen + Interoperabele modus + Deze modus stelt je in staat om van alle &appName;-functionaliteiten te genieten, terwijl je interoperabel blijft met elke SIP-dienst via point-to-point versleuteling. + Verwijderen + Laatste verbinding: + Uitloggen uit je account? + Als je je account permanent wilt verwijderen, ga naar: https://sip.linphone.org + Accountinstellingen + Open bestandskiezer + Sla LDAP-configuratie op + Sla CardDAV-configuratie op + Verwijder deze CardDAV-configuratie + Neem deel aan de conferentie + Maak een contact aan + Toon filters voor contactenlijst + Verwijder veld + Bewerk contact + Deelnemerslijst + Deel vergaderadres + Bewerk de vergadering + Plan de vergadering + Scroll naar vandaag + Begin een nieuw gesprek + Zoek naar beneden + Zoek omhoog + Gesprek wordt verwijderd + Zet geluid aan/uit voor dit gesprek + Lang indrukken om voicemail te bellen + Spraakberichten zijn beschikbaar + Kopieer tekst naar klembord + Ga naar gesprek + Gisteren + Begin + Annuleren + Telefoongesprek + SIP-adres + Deze service blijft continu actief om de app actief te houden en ervoor te zorgen dat u oproepen en berichten kunt ontvangen zonder pushmeldingen. + Zoeken naar nieuwe berichten + Bestand(en)overdracht bezig + + %s geselecteerd + %s geselecteerd + + &appName; actieve oproepmeldingen + Fout + Reageerde door %1$s op: %2$s + Login + SIP-adres bevat geen gebruikersnaam! + We hebben een verificatiecode naar uw telefoonnummer %1$s gestuurd.\n\nVoer de verificatiecode hieronder in: + Voor sommige functies is een &appName;-account vereist, zoals groepsberichten en videoconferenties.\n\nDeze functies zijn verborgen wanneer u zich registreert met een SIP-account van derden.\n\nNeem contact met ons op om deze functie in een commercieel project toe te voegen. + Pushmeldingen lijken niet beschikbaar te zijn op uw apparaat, maar ze zijn verplicht voor het aanmaken van een account in de mobiele app.\n\nWe raden u aan om in plaats daarvan een account aan te maken op ons webplatform: + Om optimaal van &appName; te kunnen genieten, moet u ons de volgende rechten verlenen: + Uw versie is up-to-date + Er is een fout opgetreden tijdens het controleren op updates + Deel link naar foutopsporingslogs via… + Zodra dit is geactiveerd, moet je de app opnieuw starten.\nDaarna wordt alle applicatiedata versleuteld en is deze alleen nog toegankelijk via de applicatie.\n\nWees voorzichtig: dit kan niet ongedaan worden gemaakt! + Beltoon wijzigen + Gesprek als gelezen markeren bij het sluiten van berichtmelding + Een beveiligde, open source en Franstalige communicatie-app. + Open-source + Een gratis en open source applicatie sinds 2001. + Bestand is geëxporteerd naar de galerij + Configuratie succesvol toegepast + Een telefoonnummer + Pushmeldingen lijken niet beschikbaar te zijn op uw apparaat, maar ze zijn verplicht om een telefoonnummeraccount in de mobiele app te herstellen. + Fout bij het proberen om de externe configuratie te downloaden en toe te passen + Ongeldige QR-code! + Contacten lezen: Hiermee geeft u uw contactpersonen weer en kunt u achterhalen wie &appName; gebruikt. + Audio opnemen: Zodat uw gesprekspartner u kan horen en om spraakberichten op te nemen. + Welke informatie &appName; verzamelt en gebruikt + Waarschuwing: eenmaal ingeschakeld kan dit niet meer worden uitgeschakeld! + %s ms + Nieuwe contacten hierin opslaan + Maximum aantal resultaten + Time-out (in seconden) + Tijd tussen twee aanvragen (in milliseconden) + Account is uitgeschakeld, je ontvangt geen oproepen of berichten. + Kies je land zodat &appName; je contacten kan koppelen. + Klik om het onderwerp van dit gesprek te bewerken + Gelieve minstens de weergavenaam en de server-URL in te vullen + Standaardindeling + Actieve spreker + Automatisch + Geavanceerde instellingen + Voeg een foto toe + Speelt de oproepopname af + Verwijder deze LDAP-configuratie + Apparaat is vertrouwd + Dit gesprek is niet beveiligd + Minimale aantal tekens om een query te starten + Naam-attributen + SIP-domein + Help ontwikkelaars bij het oplossen van problemen door logbestanden naar Crashlytics te sturen na een crash + Audiocodecs + Android-instellingen van &appName; + Eind-tot-eind versleutelde modus + Deze modus garandeert de vertrouwelijkheid van je gegevens. Onze eind-tot-eind versleutelingstechnologie biedt het hoogste niveau van beveiliging voor je communicatie. + Bewerk LDAP-server + Voeg CardDAV-adresboek toe + Bewerk CardDAV-adresboek + Mozaïek + Gebruik alleen Wi-Fi-netwerken + Sta IPv6 toe + Gebruikersinterface + Toetsenbord automatisch openen + Thema + Donker thema + Licht thema + Automatisch + Hoofdkleur + Tunnel + Host + Poort + Gebruik twee servers + Tweede host + Tweede poort + Modus + Alleen alfanumerieke tekens + URL van de bestanddelingsserver + URL van de server voor logdeling + Video-oproepen opnemen met H265/AV1 + Zal een eigen bestandsformaat gebruiken + Media-versleuteling + Media-versleuteling verplicht + Maak eind-tot-eind versleutelde vergaderingen en groepsgesprekken + Accepteer vroege media + Bel tijdens inkomend vroege mediagesprek + Sta uitgaande vroege media toe + Oproepen automatisch beantwoorden + Vertraging voordat oproep automatisch wordt beantwoord + Vertraging in milliseconden + URL voor externe provisioning + Standaard invoer audioapparaat + Standaard uitvoer audioapparaat + Mono + Stereo + Video-codecs + Sta pushmeldingen toe + Pushmeldingen zijn niet beschikbaar! + IM-versleuteling verplicht + URL van de SIP-proxyserver + Uitgaande proxy + NAT-beleid instellingen + URL van de STUN/TURN-server + Schakel TURN in + TURN-gebruikersnaam + AVPF + Vervalt (in seconden) + Conference factory URI + Schakel ICE in + TURN-wachtwoord + URI van de Audio/video conference factory + URL van de CCMP-server + URL van de E2E-versleuteling sleutel server + Bundelmodus + Gebruik CPIM in \'basis\' gesprekken + Voicemail-URI + MWI-server URI (Message Waiting Indicator) + Formateer telefoonnummers met internationaal prefix + Vervang + door 00 bij het formatteren van telefoonnummers + Wachtwoord bijwerken + Authenticatie vereist + Maak een groepsgesprek + Gesprek + Weet je zeker dat je alle gespreksgeschiedenis wilt verwijderen? + Alle oproepen worden uit de geschiedenis verwijderd + Alle oproepen worden uit de geschiedenis verwijderd + Geen contact op dit moment… + Geen SIP-contact op dit moment… + Favorieten + Alle contacten + Zie alles + Bekijk &appName; contacten + Bekijk SIP-contacten + Nieuw contact + Bewerk contact + Voornaam + Achternaam + Bedrijf + Wijzigingen zijn succesvol opgeslagen + Opslaan van wijzigingen mislukt! + Contact is succesvol aangemaakt + Wijzigingen niet opslaan? + Alle wijzigingen gaan verloren + Vul alstublieft een voornaam, achternaam of bedrijfsnaam in + Telefoonnummers en SIP-adressen + Bedrijf: + Functietitel: + Vertrouwen + Andere acties + Bewerken + Toevoegen aan favorieten + Verwijderen uit favorieten + Delen + Verwijderen + %s verwijderen? + Dit contact wordt definitief verwijderd. + Kies een nummer of een SIP-adres + Online + Online op %s + Vandaag online om %s + Gisteren online om %s + Afwezig + Niet storen + Bellen + Bericht + Video-oproep + Verifiëren + Naamloos apparaat + Verwijdering in uitvoering… + %s: + Nieuw gesprek + Nieuw groepsgesprek + Zoek contact + Maak een groepsgesprek + Geen contact en geen suggestie op dit moment… + Geen overeenkomend resultaat… + Zeg iets… + Inkomende video-oproep + Je hebt de oproep beëindigd + De gesprekspartner heeft de oproep beëindigd + Inkomende oproep voor %s + Zet %s over naar… + Huidige oproepen + Geen andere oproep + Bevestig oproepoverdracht + Je staat op het punt oproep %1$s over te dragen naar %2$s. + Doorverbinden + Nieuwe oproep + Oproepenlijst + Toetsenbord + Pauzeren + Hervatten + Opnemen + Oproep beëindigen + Indeling + Bezig + Beantwoordt naar: + Zoeken + Gesprek informatie + Functietitel + Contact aanmaken mislukt! + Deelnemers toevoegen + Nieuwe oproep + Zoek contact of gespreksgeschiedenis + Geen apparaat gevonden… + Weet je zeker dat je de geschiedenis met die persoon wilt verwijderen? + Contact is verwijderd + Geen gesprek op dit moment… + Vul alstublieft een naam in voor het gesprek + Uitgaande oproep + Berichten + Je staat op het punt een oproep te doen naar %1$s\'s apparaat %2$s.\nWil je de oproep doen? + Inkomende oproep + Inkomende video-oproep voor %s + Verbinding mislukt omdat de authenticatie ontbreekt of ongeldig is voor account \n%s.\n\nJe kunt het wachtwoord opnieuw invoeren of je accountconfiguratie controleren in de instellingen. + Wachtwoord + Kan overeenkomend account niet vinden! + Geen suggestie en geen contact op dit moment… + Onderwerp groepsoproep + Instellen onderwerp groepsoproep + Geen oproep op dit moment… + Filter wijzigen + Aantal vertrouwde apparaten: + Hallo, sluit je bij me aan op &appName;! Je kunt het gratis downloaden via %s + Nummer gekopieerd naar klembord + Vertrouwensniveau verhogen + Vertrouwensniveau + Controleer al je contactapparaten om er zeker van te zijn dat je communicatie veilig en onveranderd blijft.\nWanneer alles is geverifieerd, bereik je het maximale vertrouwensniveau. + Tekst wacht om gedeeld te worden + Markeren als gelezen + Dempen + Dempen uitschakelen + Bellen + Gesprek verwijderen + Groep verlaten + Configureer kortstondige berichten + Kortstondige berichten + Nieuwe berichten worden automatisch verwijderd zodra ze door iedereen zijn gelezen.\nKies een duur: + Uitgeschakeld + 1 minuut + 1 uur + 1 dag + 3 dagen + 1 week + + %s is aan het typen… + %s zijn aan het typen… + + Media + Vul de titel in en selecteer ten minste één deelnemer + Het verzenden van alle uitnodigingen voor de vergadering is mislukt! + Het verzenden van uitnodigingen naar sommige deelnemers van de vergadering is mislukt! + Vergaderadres gekopieerd naar klembord + Deelnemen + Annuleren + Verbinding wordt tot stand gebracht + Je zult zo meteen deelnemen + Overgaan + Deelnemen aan de vergadering mislukt! + Actief + Gepauzeerd + Gepauzeerd door externe partij + Hervatten… + Beëindigd + Eind-tot-eind versleuteld door ZRTP + Valideer ZRTP SAS opnieuw + Toestemming voor het opnemen van audio geweigerd! + Toestemming voor de camera geweigerd! + Valideer het apparaat + Oproep wordt opgenomen + %s is aan het opnemen + %s belt + %s gepauzeerde oproepen + Groepsgesprek aanmaken + Alle oproepen samenvoegen in een groepsgesprek ? + Validatie vereist + Punt-naar-punt versleuteld door SRTP + Oproep is niet versleuteld + Oproeplijst + Uw code: + Code van gesprekspartner: + Niets komt overeen + Beveiligingswaarschuwing + Probeer het opnieuw + De vertrouwelijkheid van deze oproep kan in gevaar zijn! + Oortelefoon + Luidspreker + Bluetooth (%s) + Koptelefoon + Koptelefoon + Audio + Codec: %s + Bandbreedte: %s + Verliespercentage: %s + Jitter buffer: %s + Video + Resolutie: %s + FPS: %s + FEC + Gecorrigeerde pakketten: %s + Bandbreedte: %s + Post-quantum ZRTP + Versleutelingsalgoritme: %s + Sleutel uitwisselingsalgoritme: %s + Hash-algoritme: %s + SAS-algoritme: %s + Gespreksgeschiedenis is verwijderd + Apparaat gevalideerd + Oproep wordt doorverbonden + Oproep is succesvol doorverbonden + Doorverbinden van oproep mislukt! + Gebruiker is niet gevonden + Niet-compatibele mediaparameters + Service niet beschikbaar of netwerkfout + Server time-out + Uitnodiging delen + Wachten op andere deelnemers… + Scherm delen + Deelnemers + Groepsoproep aanmaken mislukt! + Samenvoegen van oproepen mislukt! + Weet je zeker dat je deze deelnemer uit het groepsgesprek wilt verwijderen? + Deelnemer is uit het groepsgesprek verwijderd + Deelnemen… + Gepauzeerd + Deelt zijn scherm + Mozaïek + Spreker + Alleen audio + Groepsoproep op afstand + Lokale groepsoproep + Opnames + Momenteel geen opnames… + Favorieten + Nog geen favoriete contact + Toevoegen aan contacten + Bekijk contact + Telefoonnummer kopiëren + Geschiedenis verwijderen + Verwijderen + Uitnodigen + Opnieuw verzenden + Bezorgstatus + Beantwoorden + Doorsturen + Kopiëren + Downloaden + Delen + Geel + Toestemming voor meldingen niet verleend! + Toestemming om inkomende oproepen te tonen is niet verleend! + Overslaan + Wachtwoord vergeten? + Overslaan + Groen + Blauw + Rood + Roze + Paars + Geen resultaat gevonden… + Geselecteerde deelnemers zullen hier verschijnen + Account(s) verbindingsfout + Geselecteerd account is momenteel uitgeschakeld + Je bent niet verbonden met internet + Bezig met bewerking, even geduld alstublieft + Gesprekken + Contacten + Favorieten + Suggesties + Vergadering is geannuleerd! + Contact is niet vertrouwd! + Contact is vertrouwd + Contact is online + Contact is niet online + Menu openen + Teruggaan + Meldingen negeren + Wijzigingen opslaan + Menu weergeven + Klik voor meer informatie + Klik om deelnemer te verwijderen + Wachtwoordzichtbaarheid in-/uitschakelen + Breidt het onderste paneel uit/trekt het in + Beëindigt de oproep + Beantwoordt de video-oproep + Start een oproep + Start een video-oproep + Schakelt het verzenden van je camerabeeld in/uit + Wijzigt de gebruikte camera (voor/achter) + Oproep is gepauzeerd + Toont oproepstatistieken + Je bent deze oproep aan het opnemen + Verwijdert het laatste cijfer of teken + Opent het filtergebied + Sluit het filtergebied + Huidige filter wissen + Maakt het groepsgesprek aan + Start de groepsoproep + Toont het numpad + Klik om alle beschikbare opties te tonen + Deelnemer is gedempt + Deelnemers toevoegen + Speelt/pauzeert de audio-opname + Speelt/pauzeert de video-opname + Bestand delen + Bestand opslaan + Afbeelding toegevoegd aan bericht + Video toegevoegd aan bericht + Bestand toegevoegd aan bericht + Dit bericht is een reactie op een eerder bericht + Klik om de bezorgstatus te bekijken + Speelt/pauzeert de spraakbericht-opname + Verwijder dit bestand uit de bijlagen + Sluit bijlagen + Gesprek is gedempt + Tijdelijke berichten zijn ingeschakeld + Sluit de deelnemerslijst + Annuleert de opname van het spraakbericht + Stopzetting van de spraakbericht-opname + Start met opnemen van een spraakbericht + Stuurt bericht in gesprek + Opent de emoji keuzelijst + Plan een vergadering + + %s bestand wacht om gedeeld te worden + %s bestanden wachten om gedeeld te worden + + Kortstondige berichten + Documenten + Geen media gevonden… + Geen documenten gevonden… + Eind-tot-eind versleuteld gesprek + Berichten in dit gesprek zijn eind-tot-eind versleuteld. Alleen je gesprekspartner kan ze decoderen. + Gewaarborgde vertrouwelijkheid + Dit gesprek is niet versleuteld! + Voor je veiligheid is dit gesprek uitgeschakeld. + Maximaal aantal bijlagen bereikt! + Gespreksonderwerp instellen + Gespreksonderwerp bewerken + Onderwerp is verplicht + Gespreksonderwerp + Bestand openen of exporteren? + Bestand openen + Bestand exporteren + Openen als platte tekst? + Er is geen app gevonden om dit type bestand te openen.\n\nWil je proberen het als platte tekst te openen? + Openen als platte tekst + Spraakopname kan niet worden afgespeeld! + Bericht is verwijderd + Gesprek aanmaken mislukt! + Geselecteerde media niet gevonden + Gespreksonderwerp is veranderd + Tijdelijke berichten zijn ingeschakeld + Tijdelijke berichten zijn uitgeschakeld + Levensduur van tijdelijke berichten is gewijzigd + Maximale duur bereikt + Deelnemer(s) konden niet aan het gesprek worden toegevoegd + Gesprek is succesvol verwijderd + Je hebt de groep verlaten + Gesprek niet gevonden + Geen overeenkomend resultaat gevonden + Laatste overeenkomend resultaat bereikt + Foto maken + Galerij openen + Bestand kiezen + Bestand kan niet worden geopend! + Groepsleden (%s) + Deelnemers toevoegen + Beheerder + Geschiedenis verwijderen + Verwijderen uit de groep + Beheerrechten toekennen + Beheerrechten intrekken + Bekijk contactprofiel + Toevoegen aan contacten + Alle berichten worden uit de geschiedenis verwijderd + Geschiedenis is succesvol verwijderd + %s is bij het gesprek gekomen + %s heeft het gesprek verlaten + %s is nu beheerder + %s is geen beheerder meer + Contact niet gevonden + Groepsoproep starten? + Alle deelnemers ontvangen een oproep. + Je bent bij de groep gekomen + Je hebt de groep verlaten + %s is erbij gekomen + %s is vertrokken + Nieuw apparaat voor %s + Apparaat voor %s verwijderd + Nieuw onderwerp: %s + %s is beheerder + %s is geen beheerder meer + Tijdelijke berichten zijn uitgeschakeld + De levensduur van tijdelijke berichten is nu %s + LIME-identiteitssleutel is gewijzigd voor %s + Beveiligingsniveau verlaagd vanwege %s + Maximaal aantal deelnemers overschreden door %s + Media & documenten + Gedeelde media + Gedeelde documenten + Bericht doorsturen naar… + Bericht is doorgestuurd + Doorsturen van bericht is geannuleerd + Lees %s + %s ontvangen + %s verzonden + Fout %s + Reacties %s + %1$s %2$s + Klik om te verwijderen + Doorgestuurd + Vergaderuitnodiging: + Vergadering bijgewerkt: + Vergadering geannuleerd: + Spraakbericht + Er is geen vergadering gepland voor vandaag + Nieuwe vergadering + Vergadering + Uitzending + Informatie over uitzending.\nMeer leren + Titel toevoegen… + Kies de startdatum + Kies de starttijd + Kies de eindtijd + Tijdzone + Eén keer + Beschrijving toevoegen + Klik om meer deelnemers toe te voegen + Spreker toevoegen + Stuur uitnodiging naar deelnemers + Sluit nu aan in de vergadering + Organisator + Agenda-item aanmaken + Vergadering kan niet worden gevonden! + Beschrijving + Vergadering bewerken + Vergadering annuleren + Vergadering verwijderen? + Wil je de vergadering verwijderen? + Vergadering verwijderen + Vergadering is aangemaakt + Vergadering is bijgewerkt + Vergadering is geannuleerd + Vergadering geannuleerd + Vergadering inplannen mislukt! + Vergadering bewerken mislukt! + Wachten op versleuteling… + Voor uw veiligheid moeten we het apparaat van uw gesprekspartner authentiseren.\nWissel alstublieft uw codes uit: + Voor uw veiligheid moeten we het apparaat van uw gesprekspartner opnieuw authentiseren.\nWissel alstublieft opnieuw uw codes uit: + Hoortoestel (%s) + &appName; kan dit bestand niet openen.\n\nWil je het openen in een andere app (indien mogelijk), of het exporteren naar je apparaat? + Ongeldig SIP-adres, kan niet worden toegevoegd aan de conferentie + Dit bericht is doorgestuurd vanuit een ander gesprek + Scrolt naar het eerste ongelezen bericht of naar de onderkant + Bericht zal geen reactie meer zijn op een eerder bericht + Verloren pakketten: %s + Media-versleuteling + Media-versleuteling: %s + Authenticatie-algoritme: %s + + Deelnemer (%s) + Deelnemers (%s) + + Dankzij de eind-tot-eind versleutelingstechnologie in &appName; zijn de vertrouwelijkheid van berichten, oproepen en vergaderingen gegarandeerd. Niemand kan de uitgewisselde gegevens decoderen, zelfs wij niet. + Te veel deelnemers voor mozaïeklay-out + SIP-adres kopiëren + Inkomend + Gebruiker is in gesprek + Tijdelijk niet beschikbaar + %s uit het groepsgesprek verwijderen? + + %s nieuw gesproken bericht + %s nieuwe gesproken berichten + + Vergadering is bijgewerkt + Gesprek kan niet worden aangemaakt met een deelnemer die zich niet op hetzelfde domein bevindt vanwege beveiligingsbeperkingen! + Geen adres om aan contact toe te voegen + Er is geen app gevonden om dit type bestand te openen + Wil je echt alle berichten verwijderen? + Tijdelijke berichten zijn ingeschakeld + Man-in-the-middle aanval gedetecteerd voor %s + Deelnemers toevoegen + Vergadering is verwijderd + Oranje + + %s melding voor ander(en) account(s) + %s meldingen voor ander(en) account(s) + + Nieuwe deelnemerslijst bevestigen + Beantwoordt de oproep + Deelnemer is aan het spreken + Wi-Fi-only modus ingeschakeld + Dempen/dempen opheffen microfoon + Wijzigt het uitvoerapparaat voor audio + Voegt oproepen samen in een groepsgesprek + diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000000..bd75ebdc54 --- /dev/null +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,851 @@ + + +]> + + Guia do usuário do &appName; + Rede + Permitir IPv6 + Nome de exibição + Automático + Cor principal + Túnel + Servidor + Porta + Servidor secundário + Porta do servidor secundário + Modo + Desativado + Sempre + Automático + Configurações avançadas + Endereço SIP + + %s dia + %s de dias + %s dias + + Recusar + &appName; notificações de chamadas ativas + Este serviço será executado o tempo todo para manter o aplicativo ativo e permitir que você receba chamadas e mensagens sem notificações push. + Acessar a câmera: Para capturar vídeo durante chamadas de vídeo e conferências. + Ajuda + Sobre o &appName; + Filtro + Atributos SIP + Tema claro + Criptografia de mídia obrigatória + Transferência de arquivo(s) em andamento + + %s arquivo sendo baixado + %s de arquivos sendo baixados + %s arquivos sendo baixados + + O arquivo foi exportado para documentos + O endereço SIP não contém um nome de usuário! + Enviamos um código de verificação para o seu número de telefone %1$s.\n\nPor favor, insira o código de verificação abaixo: + Falha ao enviar logs de depuração + Controle adaptativo de taxa + Marcar conversa como lida ao dispensar notificação de mensagem + Suas comunicações estão seguras graças à nossa criptografia de ponta a ponta . + Aprenda a dominar todos os recursos do aplicativo, passo a passo. + Transmita seus logs de diagnóstico para facilitar a resolução de bugs. + Vibrar enquanto a chamada recebida está tocando + Sincronização foi bem-sucedida + Interface do usuário + Ajudar os desenvolvedores a solucionar problemas enviando logs para o Crashlytics após uma falha + Erro ao tentar baixar e aplicar a configuração remota + QR code inválido! + Crie uma conta com seu e-mail em: + Alguns recursos exigem uma conta &appName;, como mensagens em grupo, videoconferências…\n\nEsses recursos ficam ocultos quando você se registra com uma conta SIP de terceiros.\n\nPara habilitá-los em um projeto comercial, entre em contato conosco. + Fazer isso mais tarde + Ler contatos: Para exibir seus contatos e encontrar quem está usando o &appName;. + As notificações push não parecem estar disponíveis no seu dispositivo, mas são obrigatórias para recuperar uma conta de número de telefone no aplicativo móvel. + Quais informações o &appName; coleta e usa + Ocorreu um erro ao verificar por atualizações + Falha ao ativar o módulo de criptografia! + Uma vez ativado, você terá que reiniciar o aplicativo.\nDepois disso, todos os dados do aplicativo serão criptografados e acessíveis apenas através do aplicativo.\n\nTenha cuidado, isso não pode ser desfeito! + Mosaico + Usar apenas redes Wi-Fi + Abrir teclado automaticamente + Usar dois servidores + + %s selecionado + %s selecionados + %s selecionados + + + %s arquivo sendo enviado + %s de arquivos sendo enviados + %s arquivos sendo enviados + + Hoje + Iniciar quando o dispositivo inicializar + Manter aplicativo vivo usando Serviço + ID do dispositivo + Apenas caracteres alfanuméricos + URL do servidor de compartilhamento de arquivos + URL do servidor de compartilhamento de logs + Gravar chamadas de vídeo usando H265/AV1 + Usará um formato de arquivo proprietário + Criptografia de mídia + Criar reuniões e chamadas em grupo com criptografia de ponta a ponta + ID do dispositivo + Domínio + Nome de usuário + ID de autenticação (se diferente) + Senha + Número de telefone + ou + Próximo + Início + Ontem + Aceitar + Cancelar + Continuar + Chamar + Excluir + Instalar + Não mostrar este diálogo novamente + Não + Sim + Remover + Confirmar + &appName; notificações de chamadas recebidas + &appName; notificações de chamadas perdidas + &appName; notificação de serviço + &appName; notificações de mensagens instantâneas + Reagido por %1$s a: %2$s + Marcar como lida + Responder + Chamada perdida de %s + Chamada em grupo perdida: %s + %s chamadas perdidas + Chamada perdida + &appName; + Procurando por novas mensagens + Clique para abrir + Bem vindo + ao &appName; + Seguro + Código aberto + Um aplicativo gratuito e de código aberto desde 2001 . + Endereço SIP copiado para a área de transferência + Nova conta configurada + O arquivo foi exportado para a galeria nativa + Erro ao tentar exportar o arquivo para a galeria nativa + Erro ao tentar exportar o arquivo para documentos + O volume da mídia está baixo, talvez você não ouça nada! + Configuração aplicada com sucesso + Erro ao tentar criar o reprodutor de mídia + Termos gerais e política de privacidade + termos gerais + política de privacidade + Ao continuar, você aceita nossos %1$s e %2$s. + Confirmar número de telefone + Tem certeza de que seu número de telefone é %s? + Login + Escanear QR code + Usar uma conta SIP de terceiros + Conta SIP de terceiros + Login único + O endereço SIP é inválido! + A conta já existe + Ainda não tem uma conta? + Registrar + Confirme seu número de telefone + Número errado? + Criar + Já tem uma conta? + Transporte + Prefiro criar uma conta &appName; + Eu entendo + As notificações push não parecem estar disponíveis no seu dispositivo, mas são obrigatórias para criar uma conta no aplicativo móvel.\n\nConvidamos você a criar uma conta em nossa plataforma web: + Notificação push com token de autenticação não recebida em 5 segundos, por favor, tente novamente mais tarde + Ocorreu um erro inesperado, por favor, tente novamente mais tarde + Nome de usuário ou senha incorretos + Falha ao fazer login: o código de erro é %s + Conceder permissões + OK + Para aproveitar ao máximo o &appName; precisamos que você nos conceda as seguintes permissões: + Publicar notificações: Para ser informado quando você receber uma mensagem ou uma chamada. + Gravar áudio: Para que seu correspondente possa ouvi-lo e para gravar mensagens de voz. + Senha esquecida + Escolha como recuperar sua conta. + Você criou sua conta usando: + Um e-mail + Um número de telefone + Contatos + Chamadas + Conversas + Reuniões + Gerenciar o perfil + Conectado + Atualizando + Desativado + Conectando… + Erro + Nenhuma conta configurada ainda + Adicionar uma conta + Política de privacidade + Versão + Verificar atualização + Contribua na tradução do &appName; + Ajude a tornar o aplicativo acessível ao maior número de pessoas possível. + Avançado + Sua versão está atualizada + Atualização disponível + Uma nova versão %s está disponível. Você quer atualizar? + Sair do aplicativo + Solução de problemas + Imprimir logs no logcat + Limpar logs + Compartilhar logs + Versão do aplicativo + Versão do SDK + ID do projeto Firebase + Compartilhar link de logs de depuração usando… + Logs de depuração foram limpos + Mostrar configuração + Configurações + Segurança + Criptografar tudo + Aviso: uma vez ativado, não pode ser desativado! + Módulo de criptografia ativado + Você realmente quer criptografar tudo? + Impedir que a interface seja gravada + Chamadas + Usar cancelador de eco por software + Impede que o eco seja ouvido pela outra parte se não houver um cancelador de eco de hardware disponível + Calibrar cancelador de eco + em andamento + sem eco + %s ms + falhou + Ativar vídeo + Ativar FEC de vídeo + Iniciar gravação de chamadas automaticamente + Alterar toque + Escolher toque + Conversas + Baixar arquivos automaticamente + Tornar mídias baixadas públicas + Contatos + Adicionar servidor LDAP + Editar servidor LDAP + Adicionar agenda CardDAV + Editar agenda CardDAV + Nome de exibição + URL do servidor + Nome de usuário + Senha + Realm de autenticação + Armazenar novos contatos nela + Erro na sincronização! + Conta CardDAV removida + Por favor, preencha pelo menos o nome de exibição e a URL do servidor + URL do servidor (não pode ficar em branco) + Bind DN + Senha + Usar TLS + Base de busca (não pode ficar em branco) + Máx. de resultados + Timeout (em segundos) + Atraso entre duas consultas (em milissegundos) + Mín. de caracteres para iniciar uma consulta + Atributos de nome + Domínio SIP + Ocorreu um erro, servidor LDAP não salvo! + Reuniões + Layout padrão + Orador ativo + Tema + Tema escuro + Empresa + Cargo + Alterações foram salvas com sucesso + Falha ao salvar alterações! + Contato foi criado com sucesso + Falha ao criar contato! + Não salvar alterações? + Todas as alterações serão perdidas + Por favor, preencha o nome, sobrenome ou nome da empresa + Números de telefone e endereços SIP + Empresa: + Cargo: + Confiança + Nenhum dispositivo encontrado… + Número de dispositivos confiáveis: + Outras ações + Editar + Adicionar aos favoritos + Remover dos favoritos + Compartilhar + Excluir + Nova chamada + Pesquisar contato ou histórico de chamadas + Nenhuma chamada no momento… + Falha ao encontrar a conta correspondente! + Criar uma chamada em grupo + Você realmente quer excluir o histórico com essa pessoa? + Sobrenome + Olá, junte-se a mim no &appName;! Você pode baixá-lo gratuitamente em %s + Nenhuma sugestão e nenhum contato no momento… + Definir assunto da chamada em grupo + Assunto da chamada em grupo + Conversa + Você realmente quer excluir todo o histórico de chamadas? + Todas as chamadas serão removidas do histórico + Todas as chamadas serão removidas do histórico + Nenhum contato no momento… + Nenhum contato SIP no momento… + Alterar filtro + Favoritos + Todos os contatos + Ver todos + Ver contatos &appName; + Ver contatos SIP + Novo contato + Editar contato + Nome + O contato foi removido + Permitir notificações push + As notificações push não estão disponíveis! + Desconectado + Ativado + Aceitar mídia antecipada + Tocar ao receber chamada com mídia antecipada + Atraso em milissegundos + URL de provisionamento remoto + Baixar e aplicar + Dispositivos de áudio + Dispositivo de entrada de áudio padrão + Dispositivo de saída de áudio padrão + Codecs de áudio + mono + estéreo + Codecs de vídeo + &appName; + Um aplicativo de comunicação francês, seguro e de código aberto. + Informações da conversa + Mídia + Acompanhar o status de presença + Atender chamadas recebidas automaticamente + Atraso antes de atender chamada automaticamente + Configurações Android do &appName; + Mostrar configurações de desenvolvedor + Configurações de desenvolvedor ativadas + Configurações de desenvolvedor já ativadas + Gerenciar conta + Detalhes + Dispositivos + Falha na conexão da conta, verifique suas configurações. + Prefixo internacional + Escolha seu país para permitir que o &appName; corresponda aos seus contatos. + Configurações da conta + Escolher modo da conta + Aplicar + Modo criptografado de ponta a ponta + Modo interoperável + Este modo garante a confidencialidade dos seus dados. Nossa tecnologia de criptografia de ponta a ponta fornece o mais alto nível de segurança para suas comunicações. + Este modo permite que você aproveite todos os recursos do &appName; enquanto permanece interoperável com qualquer serviço SIP através de criptografia ponto a ponto. + Remover + Última conexão: + Sair da sua conta? + Se você deseja excluir sua conta permanentemente, acesse: https://sip.linphone.org + URL do servidor STUN/TURN + Ativar ICE + Ativar TURN + Nome de usuário TURN + Senha TURN + URL do servidor de chaves de criptografia E2E + URI do correio de voz + URI do servidor MWI (Indicador de Mensagem em Espera) + Formatar números de telefone usando prefixo internacional + Substituir + por 00 ao formatar números de telefone + Atualizar senha + Autenticação necessária + A conexão falhou porque a autenticação está ausente ou é inválida para a conta \n%s.\n\nVocê pode fornecer a senha novamente ou verificar a configuração da sua conta nas configurações. + Senha + Número copiado para a área de transferência + Aumentar nível de confiança + Você está prestes a fazer uma chamada para o dispositivo %2$s de %1$s.\nVocê quer fazer a chamada? + Escolha um número ou um endereço SIP + Online + Online em %s + Online hoje às %s + Online ontem às %s + Ausente + Não perturbe + Chamar + Mensagem + Chamada de vídeo + Verificar + Dispositivo sem nome + Nenhuma conversa no momento… + Remoção em andamento… + %s: + Texto esperando para ser compartilhado + Silenciar + Reativar som + Chamar + Excluir conversa + Sair do grupo + Configurar mensagens efêmeras + Mensagens efêmeras + Novas mensagens serão excluídas automaticamente assim que lidas por todos.\nEscolha uma duração: + Desativado + 1 minuto + Pesquisar contato + Criar uma conversa em grupo + Nenhum contato e nenhuma sugestão no momento… + Nenhum resultado correspondente… + Pesquisar + Mensagens efêmeras + Documentos + Nenhum documento encontrado… + Conversa com criptografia de ponta a ponta + As mensagens nesta conversa são criptografadas de ponta a ponta. Apenas seu correspondente pode descriptografá-las. + Confidencialidade garantida + Graças à tecnologia de criptografia de ponta a ponta no &appName;, a confidencialidade de mensagens, chamadas e reuniões é garantida. Ninguém pode descriptografar os dados trocados, nem mesmo nós. + Esta conversa não está criptografada! + Para sua segurança, esta conversa foi desativada. + O assunto é obrigatório + Assunto da conversa + Abrir ou exportar arquivo? + O &appName; não pode abrir este arquivo.\n\nVocê quer abri-lo em outro aplicativo (se possível) ou exportá-lo para o seu dispositivo? + Abrir arquivo + Exportar arquivo + Abrir como texto simples? + Nenhum aplicativo encontrado para abrir este tipo de arquivo.\n\nVocê gostaria de tentar abri-lo como texto simples? + Abrir como texto simples + O assunto da conversa mudou + Mensagens efêmeras foram ativadas + Mensagens efêmeras foram desativadas + Duração máxima atingida + Falha ao adicionar participante(s) à conversa + A conversa foi excluída com sucesso + Você saiu do grupo + Nenhum aplicativo encontrado para abrir este tipo de arquivo + A conversa não foi encontrada + Nenhum resultado correspondente encontrado + Último resultado correspondente alcançado + Tirar foto + Abrir galeria + Escolher arquivo + O arquivo não pode ser aberto! + Membros do grupo (%s) + Adicionar participantes + Admin + Excluir histórico + Remover do grupo + Dar direitos de admin + Remover direitos de admin + Ver perfil do contato + Adicionar aos contatos + O histórico foi excluído com sucesso + %s entrou na conversa + %s saiu da conversa + %s agora é admin + %s não é mais admin + Todos os participantes receberão uma chamada. + Você saiu do grupo + %s entrou + %s saiu + Nenhuma mídia encontrada… + novo dispositivo para %s + dispositivo de %s removido + novo assunto: %s + %s é admin + %s não é mais admin + Mensagens efêmeras foram ativadas + Mensagens efêmeras foram desativadas + A duração efêmera agora é %s + Chave de identidade LIME alterada para %s + Ataque man-in-the-middle detectado para %s + Nível de segurança diminuído por causa de %s + Contagem máxima de participantes excedida por %s + Mídia e documentos + Mídia compartilhada + Documentos compartilhados + Encaminhar mensagem para… + A mensagem foi encaminhada + Encaminhamento de mensagem cancelado + Lida %s + Recebida %s + Enviada %s + Erro %s + Reações %s + %1$s %2$s + Clique para remover + Encaminhada + convite para reunião: + reunião atualizada: + reunião cancelada: + mensagem de voz + Nenhuma reunião agendada para hoje + Nova reunião + Reunião + Transmissão + Informações sobre transmissão.\n Saiba mais + Adicionar título… + Escolha a data de início + Escolha a hora de início + Escolha a hora de término + Fuso horário + Uma vez + Adicionar descrição + Adicionar participantes + Clique para adicionar mais participantes + Enviar convite aos participantes + Criar evento na agenda + A reunião foi excluída + A reunião não pode ser encontrada! + Descrição + Editar reunião + Cancelar reunião + Excluir a reunião? + Você quer excluir a reunião? + Excluir reunião + A reunião foi criada + A reunião foi atualizada + A reunião foi cancelada + Reunião cancelada + Falha ao agendar reunião! + Falha ao editar reunião! + Por favor, preencha o título e selecione pelo menos um participante + Falha ao enviar todos os convites para a reunião! + Falha ao enviar convites para alguns participantes da reunião! + Endereço da reunião copiado para a área de transferência + Entrar + Cancelar + Conexão em andamento + Você entrará em breve + Falha ao entrar na reunião! + Chamada efetuada + Chamada recebida + Chamada de vídeo recebida + Você encerrou a chamada + O correspondente encerrou a chamada + Chamada recebida para %s + Chamada de vídeo recebida para %s + Transferir %s para… + Chamadas atuais + Nenhuma outra chamada + Confirmar transferência de chamada + Você está prestes a transferir a chamada %1$s para %2$s. + Transferir + Nova chamada + Lista de chamadas + Teclado + Mensagens + Pausar + Retomar + Gravar + Layout + Em andamento + Chamando + Recebendo + Ativa + Pausada + Pausada pelo remoto + Retomando… + Criar conferência + Permissão de gravação de áudio negada! + Permissão de câmera negada! + Valide o dispositivo + Para sua segurança, precisamos autenticar o dispositivo do seu correspondente.\nPor favor, troquem seus códigos: + Para sua segurança, precisamos reautenticar o dispositivo do seu correspondente.\nPor favor, troquem seus códigos novamente: + Seu código: + Código do correspondente: + Nada corresponde + Taxa de perda: %s + Alto-falante + Somente áudio + Muitos participantes para o layout em mosaico + Chamada em grupo remota + Chamada em grupo local + Gravações + Reenviar + Encaminhar + Copiar + Baixar + Compartilhar + Laranja + Amarelo + Azul + Vermelho + Rosa + Roxo + Nenhum resultado encontrado… + Os participantes selecionados aparecerão aqui + Erro de conexão da(s) conta(s) + A conta selecionada está atualmente desativada + Você não está conectado à internet + Modo Somente Wi-Fi ativado + Operação em andamento, por favor, aguarde + Conversas + Contatos + Favoritos + Sugestões + Permissão para publicar notificações não concedida! + Permissão para mostrar chamada recebida não concedida! + Pular + Esqueceu a senha? + Pular + A reunião foi atualizada + A reunião foi cancelada! + Contato é confiável + Contato não é confiável! + Contato está online + Contato não está online + Abrir menu lateral + Voltar + Dispensar notificação + Salvar alterações + Mostrar menu + Confirmar nova lista de participantes + Clique para ter mais informações + Clique para remover participante + Alterna a visibilidade da senha + Ativa/desativa o mudo do seu microfone + Você está gravando esta chamada + Remove o último dígito ou caractere + Mescla chamadas em uma conferência + Abre a área de filtro + Fecha a área de filtro + Limpa o filtro atual + Cria a conversa em grupo + Inicia a chamada em grupo + Clique para mostrar todas as opções disponíveis + Participante está mudo + Participante está falando + Adicionar participantes + Reproduz/pausa a reprodução de áudio + Reproduz/pausa a reprodução de vídeo + Compartilhar arquivo + Imagem anexada à mensagem + Vídeo anexado à mensagem + Arquivo anexado à mensagem + Esta mensagem é uma resposta a uma mensagem anterior + Esta mensagem foi encaminhada de outra conversa + Clique para ter o status de entrega + Reproduz/pausa a reprodução da mensagem de voz + Remover este arquivo dos anexos + Fechar anexos + Mensagens efêmeras estão ativadas + Rola para a primeira mensagem não lida ou para o final + Fecha a lista de participantes + Cancela a gravação da mensagem de voz + Para a gravação da mensagem de voz + Começa a gravar uma mensagem de voz + Envia mensagem na conversa + A mensagem não será mais uma resposta a uma mensagem anterior + Abre o seletor de emoji + Abre o seletor de arquivos + Clique para editar o assunto desta conversa + Silencia/reativa o som desta conversa + A conversa está sendo removida + Esta conversa não é segura + Pesquisar abaixo + Rolar para hoje + Compartilhar endereço da reunião + Entrar na conferência + Excluir esta configuração CardDAV + Salvar configuração CardDAV + Excluir esta configuração LDAP + Adicionar participantes + Você entrou no grupo + Desligar + Duração das mensagens efêmeras alterada + Configurações da conta + Criptografia de IM obrigatória + URL do servidor proxy SIP + Proxy de saída + Configurações da política NAT + Diga algo… + Respondendo a: + Usar CPIM em conversas básicas + Adicionar orador + Entrar na reunião agora + Clique para cancelar arquivos ou texto pendentes de compartilhamento + Organizador + Permitir mídia antecipada de saída + Configurações de desenvolvedor + Nenhum dispositivo encontrado… + Adicionar uma foto + Editar foto + Remover foto + Esta conta está online, todos podem ligar para você. + Esta conta está offline, provavelmente porque você não está conectado à internet agora. + A conta foi desativada, você não receberá nenhuma chamada ou mensagem. + A conta está se conectando ao servidor, por favor, aguarde… + AVPF + Expira (em segundos) + URI da fábrica de conferências + URI da fábrica de conferências de áudio/vídeo + URL do servidor CCMP + Modo bundle + Nível de confiança + Verifique todos os dispositivos do seu contato para garantir que suas comunicações sejam seguras e inalteradas.\nQuando todos forem verificados, você alcançará o nível máximo de confiança. + Excluir %s? + Este contato será removido definitivamente. + Marcar como lida + 1 hora + 1 dia + 3 dias + 1 semana + Nova conversa + Nova conversa em grupo + Por favor, insira um nome para a conversa + Número máximo de anexos atingido! + Definir assunto da conversa + Editar assunto da conversa + A gravação de voz não pode ser reproduzida! + A mensagem foi excluída + Falha ao criar conversa! + Não é possível criar conversa com um participante que não está no mesmo domínio devido a restrições de segurança! + A mídia selecionada não foi encontrada + Você realmente quer excluir todas as mensagens? + Todas as mensagens serão removidas do histórico + O contato não foi encontrado + Nenhum endereço para adicionar ao contato + Iniciar uma chamada em grupo? + Encerrada + Aguardando criptografia… + Criptografada de ponta a ponta por ZRTP + Algoritmo de autenticação: %s + Algoritmo SAS: %s + está compartilhando sua tela + Endereço SIP inválido, não pode ser adicionado à conferência + Mosaico + Mostra o teclado numérico + A conversa foi silenciada + Iniciar uma nova conversa + + %s nova mensagem de voz + %s de novas mensagens de voz + %s novas mensagens de voz + + Verde + + %s arquivo esperando para ser compartilhado + %s de arquivos esperando para ser compartilhados + %s arquivos esperando para ser compartilhados + + + %s está digitando… + %s estão digitando… + %s estão digitando… + + + %s notificação para outra(s) conta(s) + %s de notificações para outra(s) conta(s) + %s notificações para outra(s) conta(s) + + Criptografada ponto a ponto por ZRTP + Validar ZRTP SAS novamente + Validação necessária + Criptografada ponto a ponto por SRTP + A chamada não está criptografada + Lista de chamadas + A chamada está sendo gravada + %s está gravando + %s chamadas + %s chamadas pausadas + Mesclar todas as chamadas em conferência? + Alerta de segurança + Tente novamente + A confidencialidade desta chamada pode estar comprometida! + Fone de ouvido (auricular) + Alto-falante + Bluetooth (%s) + Aparelho auditivo (%s) + Headset + Fones de ouvido + Áudio + Codec: %s + Largura de banda: %s + Buffer de jitter: %s + Vídeo + Resolução: %s + FPS: %s + FEC + Pacotes perdidos: %s + Pacotes reparados: %s + Largura de banda: %s + Criptografia de mídia + Criptografia de mídia: %s + ZRTP Pós-Quântico + Algoritmo de cifra: %s + Algoritmo de acordo de chave: %s + Algoritmo de hash: %s + O histórico foi excluído + Dispositivo validado + A chamada está sendo transferida + A chamada foi transferida com sucesso + Falha na transferência da chamada! + Usuário está ocupado + Usuário não encontrado + + Participante (%s) + Participantes (%s) + Participantes (%s) + + Parâmetros de mídia incompatíveis + Serviço indisponível ou erro de rede + Tempo limite do servidor + Temporariamente indisponível + Compartilhar convite + Aguardando outros participantes… + Compartilhar tela + Participantes + Falha ao criar chamada em grupo! + Falha ao mesclar chamada! + Remover %s da conferência? + Tem certeza de que quer remover este participante da conferência? + O participante foi expulso da conferência + Entrando… + Pausado + Nenhuma gravação no momento… + Favoritos + Nenhum contato favorito ainda + Adicionar aos contatos + Ver contato + Copiar endereço SIP + Copiar número de telefone + Excluir histórico + Excluir + Convidar + Status de entrega + Responder + Atende a chamada de vídeo + Expande/retrai o painel inferior + Termina a chamada + Atende a chamada + Inicia uma chamada + Inicia uma chamada de vídeo + Ativa/desativa o envio do seu vídeo da câmera + Muda o dispositivo de saída de áudio + Muda a câmera (frontal/traseira) sendo usada + A chamada está em estado de pausa + Mostra estatísticas da chamada + Salvar arquivo + Pesquisar acima + Agendar uma reunião + Agendar a reunião + Editar a reunião + Lista de participantes + Dispositivo é confiável + Editar contato + Remover campo + Mostrar filtros da lista de contatos + Criar um contato + Salvar configuração LDAP + Reproduz a gravação da chamada + Ir para a conversa + Copiar texto para a área de transferência + Mensagens de voz estão disponíveis + Pressione e segure para ligar para o correio de voz + Sair + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000000..fcb39fa4c5 --- /dev/null +++ b/app/src/main/res/values-ru/strings.xml @@ -0,0 +1,856 @@ + + +]> + + Отображаемое имя + SIP-адрес + Имя пользователя + Пароль + Номер телефона + или + Следующий + Начало + Сегодня + Вчера + + %s день + %s дня + %s дней + %s дней + + + %s выбран + %s выбрано + %s выбрано + %s выбрано + + Принять + Отменить + Продолжить + Звонок + Удалить + Больше не показывать этот диалог + Нет + Да + Удалить + Уведомления о пропущенных звонках &appName; + Служебные уведомления &appName; + Уведомления о мгновенных сообщениях &appName; + Отреагировал %1$s на: %2$s + Ответить + Пропущенный звонок от %s + %s пропущенных звонков + Пропущенный звонок + &appName; + Поиск новых сообщений + &appName; + Выполняется передача файлов + + Скачивается %s файл + Скачиваются %s файла + Скачиваются %s файлов + Скачиваются %s файлов + + Нажмите, чтобы открыть + в &appName; + Открытый исходный код + Бесплатное приложение с открытым исходным кодом с 2001 года. + SIP-адрес скопирован в буфер обмена + Новая учетная запись настроена + Файл экспортирован в документы + Ошибка при попытке экспортировать файл в документы + Конфигурация успешно применена + Ошибка при попытке загрузить и применить удаленную конфигурацию + Ошибка при попытке создания медиаплеера + общие положения + политика конфиденциальности + Подтвердите номер телефона + Вы уверены, что ваш номер телефона %s? + Логин + Сканировать QR-код + Неверный QR-код! + Использовать сторонний SIP-аккаунт + Единый вход + SIP-адрес не содержит имя пользователя! + Учетная запись уже существует + Еще нет аккаунта? + Зарегистрироваться + Создать + Создайте учетную запись, указав свой адрес электронной почты: + У вас уже есть аккаунт? + Транспорт + Я предпочитаю создать учетную запись &appName; + URL-адрес сервера + Имя пользователя + Пароль + Auth realm + Синхронизация прошла успешно + Ошибка синхронизации! + ИД устройства + Домен + Идентификатор аутентификации (если отличается) + Отказать + Уведомления о входящих звонках &appName; + Ваши коммуникации защищены благодаря нашему сквозному шифрованию. + Громкость мультимедиа низкая, вы можете ничего не услышать! + Общие положения и политика конфиденциальности + Продолжая, вы принимаете наши %1$s и %2$s. + Недействительный SIP-адрес! + Подтвердите свой номер телефона + Неправильный номер? + Отображаемое имя + Установить + Подтвердить + Уведомления об активных звонках &appName; + Пропущенный групповой звонок: %s + + Отправляется %s файл + Отправляются %s файла + Отправляются %s файлов + Отправляются %s файлов + + Мы отправили код подтверждения на ваш номер телефона %1$s.\n\nВведите код подтверждения ниже: + Для некоторых функций требуется учетная запись &appName;, например, групповые сообщения, видеоконференции…\n\nЭти функции скрыты при регистрации с использованием стороннего SIP-аккаунта.\n\nЧтобы включить её в коммерческом проекте, свяжитесь с нами. + Эта служба будет работать постоянно, чтобы поддерживать приложение в рабочем состоянии и позволять вам принимать звонки и сообщения без push-уведомлений. + Отметить как прочитанное + Добро пожаловать + Защищённое, с открытым исходным кодом и французское приложение для общения. + Безопасный + Файл экспортирован в собственную галерею + Ошибка при попытке экспортировать файл в собственную галерею + Сторонний SIP-аккаунт + Хранить недавно созданные контакты здесь + Учетная запись CardDAV удалена + URL-адрес сервера (не может быть пустым) + Привязать DN + Пароль + База поиска (не может быть пустой) + База поиска (не может быть пустой) + Максимальное количество результатов + Задержка между двумя запросами (в миллисекундах) + Минимальное количество символов для запроса + Атрибуты SIP + SIP-домен + Произошла ошибка, сервер LDAP не сохранен! + Конференции + Отображать по умолчанию + Показать говорящего + Мозайка + Сеть + Разрешить IPv6 + Пользовательский интерфейс + Автоматически открывать панель набора номера + Тема + Тёмная тема + Светлая тема + Авто + Туннель + Хост + Порт + Использовать два сервера + Второй порт + Режим + Отключено + Всегда + Авто + Расширенные настройки + Запускать при загрузке устройства + Поддерживайте работу приложения с помощью Сервиса + Только буквенно-цифровые символы + URL-адрес сервера обмена файлами + Запись видеозвонков с использованием H265/AV1 + Шифрование медиа + Обязательное шифрование медиа + Создавать сквозные зашифрованные встречи и групповые звонки + Принимать ранний поток + Разрешить исходящие ранний поток + Автоматический ответ на входящие звонки + Задержка перед автоответом на звонок + Задержка в миллисекундах + Загрузите и применить + Аудиоустройства + Аудиоустройство вывода по умолчанию + Аудиокодеки + моно + стерео + Параметры &appName; в Android + Настройки разработчика + Показать настройки разработчика + Настройки разработчика уже включены + Управлять аккаунтом + Детали + Устройства + Устройство не найдено… + Удалить изображение + Этот аккаунт в сети, каждый может вам позвонить. + Аккаунт подключается к серверу, пожалуйста, подождите… + Не удалось подключиться к аккаунту, проверьте настройки. + Международный префикс + Настройки аккаунта + Выход + Выберите режим аккаунта + Применить + Режим сквозного шифрования + Режим взаимодействия + Удалить + Последнее соединение: + Выйти из своего аккаунта? + Настройки аккаунта + Разрешить push-уведомления + Push-уведомления недоступны! + Обязательное шифрование мгновенных сообщений + Исходящий прокси + Настройки политики NAT + SIP-прокси сервер + Включить ICE + Включить TURN + Пароль TURN + AVPF + Истекает (в секундах) + URL-адрес сервера CCMP + URL сервер ключей шифрования E2E + Режим объединения + Используйте CPIM в «базовых» беседах + URI голосовой почты + URI сервера MWI (Message Waiting Indicator) + Использовать TLS + Основной цвет + Звонок во время входящего раннего потока + Настройки разработчика включены + Аккаунт отключен, вы не получите никаких звонков или сообщений. + Выберите страну, чтобы разрешить &appName; сопоставлять ваши контакты. + Этот режим позволяет вам пользоваться всеми функциями &appName;, сохраняя при этом совместимость с любым SIP-провайдером посредством шифрования «точка-точка». + Тайм-аут (в секундах) + URL-адрес удалённого конфигурирования + Этот режим гарантирует конфиденциальность ваших данных. Наша технология сквозного шифрования обеспечивает наивысший уровень безопасности для ваших коммуникаций. + STUN/TURN сервер + Формат телефонных номеров с использованием международного префикса + Использовать только Wi-Fi сети + URI фабрики конференции + Пожалуйста, заполните как минимум отображаемое имя и URL-адрес сервера + Второй хост + Помогите разработчикам устранить неполадки, отправив журналы в Crashlytics после сбоя + URL сервера обмена журналами + Аудиоустройство ввода по умолчанию + Атрибуты имени + Идентификатор устройства + Будет использовать проприетарный формат файла + Видеокодеки + Редактировать изображение + Добавить изображение + Если вы хотите удалить свою учетную запись навсегда, перейдите по ссылке: https://sip.linphone.org + Имя пользователя TURN + URI фабрики аудио/видео конференции + Я понял + Похоже, push-уведомления недоступны на вашем устройстве, но они обязательны для создания учетной записи в мобильном приложении.\n\nВместо этого мы предлагаем вам создать аккаунт на нашей веб-платформе: + Push-уведомление с токеном аутентификации не получено в течение 5 секунд, повторите попытку позже + Произошла непредвиденная ошибка, попробуйте еще раз позже + Неправильное имя пользователя или пароль + Не удалось войти: код ошибки %s + Предоставить разрешения + ОК + Сделайте это позже + Чтение контактов: Чтобы отобразить ваши контакты и узнать, кто использует &appName;. + Запись звука: Чтобы ваш собеседник мог слышать вас и записывать голосовые сообщения. + Доступ к камере. Для записи видео во время видеозвонков и конференций. + Пароль забыт + Выберите способ восстановления вашей учетной записи. + Номер телефона + Контакты + Звонки + Беседы + Встречи + Управление профилем + Подключен + Подключение… + Ошибка + Аккаунт пока не настроен + Добавить аккаунт + О &appName; + Какую информацию собирает и использует &appName; + Версия + Проверить обновления + Расширенный + Произошла ошибка при проверке обновлений + Ваша версия самая последняя + Доступно обновление + Выйти из приложения + Поделиться логами + Поделитесь ссылкой на журналы отладки с помощью… + Журналы отладки были очищены + Показать конфигурацию + Настройки + Безопасность + Зашифровать все + Не удалось включить модуль шифрования! + Модуль шифрования включен + Звонки + Устраняет появление эха, слышимое на другой стороне, если отсутствует аппаратный эхоподавитель + Калибровка эхоподавления + эхо не обнаружено + %s мс + калибровка не удалась + Адаптивный контроль скорости + Включить видео + Включить видео FEC + Автоматически начинать запись звонков + Изменить рингтон + Выбрать рингтон + Беседы + Автоматическая загрузка файлов + Отметить беседу как прочитанную при закрытии уведомления о сообщении + Контакты + Добавить LDAP-сервер + Редактировать LDAP-сервер + Добавление адресной книги CardDAV + Изменение адресной книги CardDAV + Заменить + на 00 при формировании телефонных номеров + Обновить пароль + Требуется аутентификация + Подключение не удалось, так как аутентификация отсутствует или недействительна для учетной записи\n%s\n\nВы можете снова указать пароль или проверить конфигурацию аккаунта в настройках. + Пароль + Не удалось найти соответствующий аккаунт! + Новый вызов + Найти контакт или историю звонков + Создать групповой звонок + Задать тему группового звонка + Тема группового звонка + Беседа + Вы действительно хотите удалить всю историю звонков? + Вы действительно хотите удалить историю с этим человеком? + Все звонки будут удалены из истории + Сейчас нет ни одного контакта… + Изменить фильтр + Избранное + Все контакты + Смотреть все + Смотреть контакты &appName; + Смотреть контакты SIP + Новый контакт + Изменить контакт + Фамилия + Должность + Организация + Контакт был успешно создан + Не удалось создать контакт! + Не сохранять изменения? + Все изменения будут потеряны + Номера телефонов и SIP-адреса + Организация: + Должность: + Доверять + Устройство не найдено… + Количество доверенных устройств: + Изменить + Добавить в избранное + Удалить из избранного + Поделиться + Удалить + Контакт был удален + Номер скопирован в буфер обмена + Повысить уровень доверия + Уровень доверия + Удалить %s? + Этот контакт будет окончательно удален. + Выберите номер или SIP-адрес + В сети + В сети в %s + Вчера был в сети в %s + Сегодня был в сети в %s + Отсутствует + Не беспокоить + Звонок + Сообщение + Проверить + Устройство без названия + На данный момент нет бесед… + %s: + Текст ожидает отправки + Отключить звук + Позвонить + Удалить беседу + Покинуть группу + Временные сообщения + Отключить + 1 минута + 1 час + 1 день + 3 дня + 1 неделя + Новая беседа + Новая групповая беседа + Введите название для беседы + Найти контакт + Создать групповой беседу + На данный момент нет контактов и предложений… + Скажи что-нибудь… + Добавить участников + Ответить на: + Найти + Временные сообщения + Медиа + Документы + Медиа не найдены… + Документы не найдены… + + %s сочиняет… + %s сочиняют… + %s сочиняют… + %s сочиняют… + + Этот разговор не зашифрован! + В целях вашей безопасности эта беседа была отключена. + Достигнуто максимальное количество вложений! + Установить тему беседы + Изменить тему беседы + Тема является обязательной + Тема беседы + Открыть или экспортировать файл? + &appName; не удается открыть этот файл.\n\nХотите открыть его в другом приложении (если возможно) или экспортировать на свое устройство? + Открыть файл + Открыть как обычный текст? + Открыть как обычный текст + Запись разговора не может быть воспроизведена! + Сообщение удалено + Не удалось создать беседу! + Выбранный медиафайл не найден + Тема беседы изменилась + Включены временные сообщения + Временные сообщения отключены + Изменено время жизни временных сообщений + Максимальная продолжительность достигнута + Беседа была успешно удалена + Вы покинули группу + Не найдено ни одного приложения для открытия этого типа файла + Достигнут последний результат соответствия + Сфотографировать + Открыть галерею + Выбрать файл + Участники группы (%s) + Добавить участников + Администратор + Удалить из группы + Предоставить права администратора + Удалить права администратора + Добавить в контакты + Вы действительно хотите удалить все сообщения? + История успешно удалена + %s присоединился к беседе + %s покинул беседу + Начать групповой звонок? + Всем участникам будет сделан звонок. + Вы присоединились к группе + Вы покинули группу + %s присоединился + %s ушел + новое устройство для %s + новая тема: %s + %s больше не администратор + Временные сообщения включены + Время жизни сообщений теперь составляет %s + Ключ идентификации LIME изменен для %s + Уровень безопасности снижен из-за %s + Максимальное количество участников превышено на %s + Медиа и документы + Общие медиа + Общие документы + Переслать сообщение… + Пересылка сообщения отменена + Читать %s + Получено %s + Отправлено %s + Ошибка %s + %1$s %2$s + Нажмите, чтобы удалить + Переслано + обновленная информация о встрече: + встреча отменена: + голосовое сообщение + Новая встреча + Встреча + Транслировать + Информация о трансляции.\nУзнать больше + Добавить заголовок… + Выберите дату начала + Выберите время окончания + Часовой пояс + Один раз + Добавить описание + Добавить участника + Отправить приглашение участникам + Присоединиться к встрече сейчас + Органайзер + Встреча была удалена + Встреча не найдена! + Описание + Редактировать встречу + Отменить встречу + Удалить встречу? + Вы хотите удалить встречу? + Удалить встречу + Встреча была обновлена + Встреча отменена + Встреча была создана + Не удалось запланировать встречу! + Не удалось отправить все приглашения на встречу! + Адрес встречи скопирован в буфер обмена + Присоединиться + Отмена + Соединение в процессе + Вы присоединитесь в ближайшее время + Не удалось присоединиться к встрече! + Исходящий звонок + Входящий звонок + Входящий видеозвонок + Участник завершил звонок + Входящий вызов для %s + Входящий видеозвонок для %s + Текущие звонки + Других звонков нет + Подтвердите перевод вызова + Список вызовов + Номеронабиратель + Сообщения + Пауза + Продолжить + Запись + Положить трубку + В процессе + Идёт вызов + Входящий + Активный + Приостановлено + Приостановлено удаленно + Завершено + Ожидание шифрования… + Сквозное шифрование ZRTP + Повторная проверка ZRTP SAS + Шифрование точка-точка по SRTP + Звонок не зашифрован + Звонок записывается + %s записывает + %s звонков + Объединить все звонки в конференцию? + Создать конференцию + Доступ к видеозаписи отклонён! + Проверить устройство + Для вашей безопасности нам необходимо повторно аутентифицировать ваше устройство.\nПожалуйста, повторно обменяйтесь вашими кодами: + Ваш код: + Код собеседника: + Ничего не совпадает + Предупреждение о безопасности + Конфиденциальность звонка может быть нарушена! + Наушники + Bluetooth (%s) + Слуховой аппарат (%s) + Наушники + Аудио + Кодек: %s + Пропускная способность: %s + Коэффициент потерь: %s + Jitter-буфер: %s + Видео + Кадров в секунду: %s + FEC + Потеря пакетов: %s + Исправлено пакетов: %s + Пропускная способность: %s + Шифрование медиа: %s + Пост-квантовый ZRTP + Алгоритм шифрования: %s + Алгоритм согласования ключей: %s + Алгоритм SAS: %s + История была удалена + Устройство проверено + Вызов переводится + Перевод вызова не удался! + Пользователь занят + Пользователь не найден + Несовместимые параметры медиа + Тайм-аут сервера + Временно недоступно + Поделиться приглашением + Ожидание остальных участников… + Поделиться экраном + Участники + Не удалось объединить вызов! + + (%s) участник + (%s) участника + (%s) участников + (%s) участников + + Вы уверены, что хотите удалить этого участника из конференции? + Участник был исключен из конференции + Приостановлено + делится своим экраном + Мозаика + Только аудио + Текущий участник + Слишком много участников для мозаичной отображения + Удаленный групповой вызов + Местный групповой звонок + Записи звонков + На данный момент записи нет… + Избранное + Добавить в контакты + Смотреть контакт + Копировать SIP-адрес + Удалить историю + Удалить + Пригласить + Отправить повторно + Статус доставки + Ответить + Переслать + Копировать + Поделиться + Оранжевый + Жёлтый + Синий + Красный + Розовый + Фиолетовый + Выбранные участники появятся здесь + Вы не подключены к интернету + Выбранный аккаунт сейчас отключен + Включен режим «Только Wi-Fi» + Операция выполняется, пожалуйста, подождите + Беседы + Контакты + Избранное + Предложения + Разрешение на отправку уведомлений не предоставлено! + + %s новое голосовое сообщение + %s новых голосовых сообщений + %s новых голосовых сообщений + %s новых голосовых сообщений + + Пропустить + Пропустить + Встреча была обновлена + Встреча была отменена! + Доверенный контакт + Контакт в сети + Контакт не в сети + Вернуться + Отклонить уведомление + Сохранить изменения + Показать меню + Нажмите, чтобы получить больше информации + Нажмите, чтобы удалить участника + Завершение вызова + Отвечает на вызов + Начинает звонок + Начинает видеозвонок + Включает/отключает отправку видео с камеры + Изменяет выходное аудиоустройство + Изменяет используемую камеру (переднюю/заднюю) + Вызов приостановлен + Вы записываете этот звонок + Удаляет последнюю цифру или символ + Объединяет звонки в конференцию + Закрывает область фильтра + Очистить текущий фильтр + Создает групповую беседу + Показывает цифровую клавиатуру + Нажмите, чтобы показать все доступные варианты + Участник говорит + Добавить участников + Воспроизведение/приостановка воспроизведения звука + Поделиться файлом + Сохранить файл + Изображение прикреплено к сообщению + Видео прикреплено к сообщению + Файл прикреплен к сообщению + Это сообщение было переслано из другой беседы + Нажмите, чтобы узнать статус доставки + Удалить этот файл из вложений + Закрыть вложения + Беседа была прервана + Включены временные сообщения + Закрывает список участников + Отменяет запись голосового сообщения + Начинает запись голосового сообщения + Отправляет сообщение в беседу + Открывает выбор эмодзи + Открывает средство выбора файлов + Нажмите, чтобы изменить тему этой беседы + Включает/выключает эту беседу + Беседа удаляется + Поиск вверх + Поиск вниз + Начать новую беседу + Прокрутить до сегодняшнего дня + Запланировать встречу + Редактировать встречу + Поделиться адресом встречи + Список участников + Устройство является доверенным + Удалить поле + Показать фильтры списка контактов + Создать контакт + Удалить текущую конфигурацию CardDAV + Сохранить конфигурацию CardDAV + Удалить текущую конфигурацию LDAP + Сохранить конфигурацию LDAP + Воспроизведение записи звонка + Перейти к беседе + Копировать текст в буфер обмена + Доступны голосовые сообщения + Нажмите и удерживайте, чтобы позвонить на голосовую почту + Никаких предложений и контактов на данный момент… + На данный момент звонка нет… + Не удалось сохранить изменения! + Пожалуйста, заполните имя, фамилию или название организации + Удаление в процессе… + Гарантированная конфиденциальность + Все сообщения будут удалены из истории + Контакт не найден + Нет адреса для добавления в контакт + Не удалось отправить приглашения некоторым участникам встречи! + Вы завершили звонок + Вы собираетесь перевести вызов %1$s на %2$s. + Разрешение: %s + Шифрование медиаданных + Хэш-алгоритм: %s + Звонок успешно переведен + Удалить %s из конференции? + Включает/выключает видимость пароля + Расширяет/втягивает нижний лист + Распечатать логи в logcat + Очистить логи + Имя + Видеозвонок + Настроить временные сообщения + Информация о беседе + Не найдено ни одного приложения для открытия этого типа файла.\n\nХотите попробовать открыть его как обычный текст? + Изменения были успешно сохранены + + %s файл ожидает отправки + %s файла ожидают отправки + %s файлов ожидают отправки + %s файлов ожидают отправки + + Включить звук + Беседа не найдена + устройство для %s удалено + %s администратор + Временные сообщения отключены + Реакции %s + Добавить участников + Создать событие календаря + Возобновление… + %s приостановленных вызовов + Доступ к аудиозаписи отклонён! + Для вашей безопасности нам необходимо аутентифицировать ваше устройство.\nПожалуйста, обменяйтесь кодами: + Все звонки будут удалены из истории + На данный момент нет SIP-контактов… + Другие действия + Привет, присоединяйся ко мне на &appName;! Ты можешь скачать его бесплатно на %s + Отметить как прочитанное + Новые сообщения будут автоматически удаляться после прочтения всеми.\nВремя жизни сообщения: + Нет соответствующих результатов… + Сквозное шифрование беседы + Сообщения в этой беседе зашифрованы end-to-end шифрованием. Расшифровать их может только ваш собеседник. + Экспортировать файл + Просмотреть профиль контакта + Вы собираетесь позвонить на устройство %2$s пользователя %1$s.\nХотите позвонить? + Проверьте все свои контактные устройства, чтобы убедиться, что ваша связь будет защищена в неизменном виде.\nКогда все будет проверено, вы достигнете максимального уровня доверия. + Благодаря технологии сквозного шифрования в &appName;, гарантируется конфиденциальность сообщений, звонков и встреч. Никто не может расшифровать обмен данными, даже мы сами. + Не удалось добавить участника(ов) к беседе + Подходящего результата не найдено + Файл не может быть открыт! + Невозможно создать беседу с участником, находящимся не в том же домене, из-за ограничений безопасности! + Удалить историю + %s теперь администратор + %s больше не администратор + Алгоритм аутентификации: %s + Пока нет избранного контакта + Скачать + Зелёный + Обнаружена атака типа «человек посередине» для %s + Сообщение было переслано + На сегодня встреча не запланирована + приглашение на встречу: + Нажмите, чтобы добавить больше участников + Не удалось изменить встречу! + Пожалуйста, заполните заголовок и выберите хотя бы одного участника + Формат + Требуется проверка + Список вызовов + Попробуйте еще раз + Громкая связь + Выберите время начала + Внимание: после включения отключить невозможно! + калибровка началась + Вибрация при входящем звонке + Встреча была отменена + Перевести %s в… + Перевод звонка + Новый вызов + Отвечает на видеовызов + Отключить/включить микрофон + Начинает групповой звонок + У участника отключен микрофон + Это сообщение является ответом на предыдущее сообщение + Воспроизводит/приостанавливает воспроизведение голосового сообщения + Эта беседа не защищена + Отправлять уведомления: Чтобы получать информацию о получении сообщения или звонка. + Вы создали свой аккаунт, используя: + Не удалось загрузить журналы отладки + Вы действительно хотите всё зашифровать? + Сделать загруженные медиафайлы общедоступными + Гарнитура + Сервис недоступен или ошибка сети + Не удалось создать групповой звонок! + Подключаюсь… + Неверный SIP-адрес, невозможно добавить в конференцию + Скопировать номер телефона + Подтвердить список новых участников + Показывает статистику звонков + Открывает область фильтра + Прокрутить до первого непрочитанного сообщения или до конца + Останавливает запись голосового сообщения + Сообщение больше не будет ответом на предыдущее сообщение + Похоже, что push-уведомления недоступны на вашем устройстве, но они необходимы для восстановления номера телефона аккаунта в мобильном приложении. + Отключено + Политика конфиденциальности + Внесите свой вклад в перевод &appName; + Устранение неполадок + Результатов не найдено… + Ошибка подключения к аккаунту(ам) + + %s уведомление для других аккаунта(ов) + %s уведомления для других аккаунта(ов) + %s уведомлений для других аккаунта(ов) + %s уведомлений для других аккаунта(ов) + + Разрешение на отображение входящего вызова не предоставлено! + Забыли пароль? + Ненадёжный контакт! + Чтобы в полной мере насладиться &appName; нам нужно, чтобы вы предоставили следующие разрешения: + Помощь + Доступна новая версия %s. Хотите обновить? + Версия приложения + Версия SDK + ID проекта Firebase + После активации вам придется перезапустить приложение.\nПосле этого все данные приложения будут зашифрованы и доступны только через него.\n\nБудьте осторожны, это невозможно отменить! + Предотвратить запись интерфейса + Использовать программное подавление эха + Воспроизведение/приостановка воспроизведения видео + Запланируйте встречу + Изменить контакт + Присоединиться к конференции + Электронная почта + Обновляется + Открыть меню ящика + Отправьте свои диагностические журналы, чтобы облегчить устранение ошибок. + Включено + Нажмите, чтобы отменить файлы или текст в ожидании обмена + &appname; Руководство пользователя + Узнайте, как освоить все функции приложения, шаг за шагом. + Помогите сделать приложение доступным как можно большим количеством людей. + diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml new file mode 100644 index 0000000000..88b4846384 --- /dev/null +++ b/app/src/main/res/values-sk/strings.xml @@ -0,0 +1,848 @@ + + +]> + + ID zariadenia + Zobrazované meno + Doména + Používateľské meno + Heslo + Telefónne číslo + alebo + Ďalej + Začať + Dnes + Včera + + %s deň + %s dní + %s dní + + + %s vybrané + %s vybraných + %s vybraných + + Odmietnuť + Prijať + Zrušiť + Pokračovať + Volať + Vymazať + Inštalovať + Nie + Áno + Odstrániť + Potvrdiť + Oznámenia o aktívnych hovoroch &appName; + Oznámenia o zmeškaných hovoroch &appName; + Oznámenia služby &appName; + Oznámenia o okamžitých správach &appName; + Reakcia používateľa %1$s to: %2$s + SIP adresa + ID pre overenie (ak je odlišné) + Služba bude bežať trvalo, aby zabezpečila funkčnosť aplikácie a príjem hovorov a správ bez použitia push oznámení. + Tento dialóg už nezobrazovať + Oznámenia o prichádzajúcich hovoroch &appName; + Označiť ako prečítané + Odpovedať + Zmeškaný hovor od %s + Zmeškaný skupinový hovor od %s + %s zmeškaných hovorov + Zmeškaný hovor + &appName; + Hľadanie nových správ + &appName; + Prebieha prenos súboru(-ov) + + %s súbor sa nahráva + %s súborov sa nahráva + %s súborov sa nahráva + + + %s súbor sa sťahuje + %s súborov sa sťahuje + %s súborov sa sťahuje + + Kliknite pre otvorenie + Vitajte + v &appName; + Bezpečná , otvorená a francúzska komunikačná aplikácia. + Zabezpečená + Vaša komunikácia je bezpečná vďaka nášmu koncovému šifrovaniu. + Otvorená + Bezplatná a otvorená aplikácia od roku 2001. + SIP adresa skopírovaná do schránky + Nový účet bol nastavený + Súbor bol exportovaný do systémovej galérie + Chyba pri exportovaní súboru do systémovej galérie + Súbor bol exportovaný do dokumentov + Chyba pri exportovaní súboru do dokumentov + Hlasitosť médií je nízka, nemusíte nič počuť! + Chyba pri pokuse o vytvorenie prehrávača médií + všeobecné podmienky + Všeobecné podmienky a zásady ochrany súkromia + zásady ochrany súkromia + Pokračovaním akceptujete naše %1$s a %2$s. + Potvrdiť telefónne číslo + Prihlásenie + Neplatný QR kód! + Naskenovať QR kód + Použiť SIP účet tretej strany + SIP účet tretej strany + Jednotné prihlásenie + Neplatná SIP adresa! + SIP adresa neobsahuje používateľské meno! + Účet už existuje + Nemáte ešte účet? + Registrácia + Nesprávne číslo? + Vytvoriť + Vytvorte účet pomocou svojej e-mailovej adresy na: + Máte už účet? + Prenos + Preferujem vytvorenie &appName; účtu + Rozumiem + Push notifikácia s autentifikačným tokenom nebola prijatá behom 5 sekúnd, skúste to, prosím, neskôr znovu + Nastala neočakávaná chyba, skúste to, prosím, neskôr znovu + Konfigurácia bola úspešne nastavená + Došlo k chybe pri pokuse o stiahnutie a použitie vzdialenej konfigurácie + Poslali sme Vám overovací kód na Vaše telefónne číslo %1$s,\n\nZadajte nižšie, prosím, verifikačný kód: + Ste si istý, že Vaše telefónne číslo je %s? + Push notifikácie sa na Vašom zariadení zdajú byť nedostupné, ale sú neodmysliteľné pre vytvorenie účtu v mobilnej aplikácii.\n\nPozývame Vás preto, aby ste si namiesto toho vytvorili účet na našej webovej platforme: + Nesprávne používateľské meno alebo heslo + Potvrďte svoje telefónne číslo + Niektoré funkcie ako skupinové správy alebo videokonferencie vyžadujú účet &appName; \n\nTieto funkcie sú skryté, pokiaľ sa zaregistrujete pomocou SIP adresy tretej strany.\n\nPre aktiváciu v komerčnom prostredí nás, prosím, kontaktujte. + Prihlásenie sa nepodarilo: chybový kód je %s + Udeliť oprávnenia + OK + Vykonať neskôr + Pre plné využívanie &appName; potrebujeme udelenie nasledovných oprávnení: + Čítanie kontaktov: Pre zobrazenie Vašich kontaktov a zistení, kto používa &appName;. + Odosielanie notifikácií: Aby ste boli informovaný, keď obdržíte správa alebo hovor. + Prístup ku kamere: Pre zachytenie videa počas videohovorov a konferencií. + Zabudnuté heslo + Zvoľte spôsob obnovy svojho účtu. + Váš účet bol vytvorený pomocou: + E-mailu + Telefónneho čísla + Push notifikácie sú na Vašom zariadení nedostupné, avšak sú nevyhnutné pre obnovu účtu s telefónnym číslom v mobilnej aplikácii. + Kontakty + Hovory + Konverzácie + Schôdzky + Spravovať profil + Pripojené + Obnovujem + Zakázané + Pripájanie… + Chyba + Pridať účet + Pomoc + O aplikácii &appName; + Sprievodca &appName; + Naučte sa krok za krokom ovládať všetky funkcie aplikácie. + Zásady ochrany súkromia + Verzia + Pomôžte sprístupniť aplikáciu čo najväčšiemu počtu ľudí. + Pokročilé + Vaša verzia je aktuálna + Je dostupná nová aktualizácia + Nová verzia %s je dostupná. Chcete ju aktualizovať? + Ukončiť aplikáciu + Riešenie problémov + Odošlite svoje diagnostické záznamy, aby ste uľahčili riešenie chýb. + Vyčistiť záznamy + Zdieľať záznamy + Verzia aplikácie + Verzia SDK + ID Firebase project + Zdieľať odkaz na ladiace záznamy pomocou… + Ladiace záznamy boli vyčistené + Zobraziť konfiguráciu + Nastavenia + Zabezpečenie + Šifrovať všetko + Varovanie: po zapnutí sa už nedá zrušiť! + Modul šifrovania zapnutý + Naozaj mienite šifrovať všetko? + Zabrániť nahrávaniu rozhrania aplikácie + Hovory + Použiť softvérové potlačenie ozveny + Zabraňuje, aby ozvenu bolo počuť na vzdialenej strane, ak nie je k dispozícii hardvérové potlačenie ozveny + Kalibrovať potlačenie ozveny + prebieha + bez ozveny + %s ms + zlyhalo + Povoliť video + Povoliť FEC pre video + Automaticky spustiť nahrávanie hovorov + Zmeniť vyzváňací tón + Vybrať vyzváňací tón + Automaticky sťahovať súbory + Označiť konverzáciu ako prečítanú pri zavretí oznámenia o správe + Kontakty + Pridať LDAP server + Upraviť LDAP server + Pridať adresár CardDAV + Upraviť adresár CardDAV + Zobrazované meno + URL adresa servera + Používateľské meno + Heslo + Ukladať novovytvorené kontakty sem + Synchronizácia bola úspešná + Synchronizácia zlyhala! + Účet CardDAV bol odstránený + Zapnuté + Bind DN (pripájací identifikátor) + Heslo + Použiť TLS + Počiatočný bod hľadania (nesmie byť prázdne) + URL adresa servera (nesmie byť prázdne) + Počiatočný bod hľadania (nesmie byť prázdne) + Maximálny počet výsledkov + Časový limit (v sekundách) + Minimálny počet znakov pre spustenie dotazu + Atribúty názvu + Atribúty SIP + Doména SIP + Nastala chyba, server LDAP nebol uložený! + Schôdzky + Konverzácie + Aktívny hovoriaci + Mozaika + Sieť + Povoliť IPv6 + Používateľské rozhranie + Automaticky otvoriť číselník (numerickú klávesnicu) + Motív + Tmavý motív + Svetlý motív + Automaticky + Hlavná farba + Tunel + Port + Použiť dva servery + Druhý hostiteľ + Druhý port + Režim + Zakázané + Vždy + Automaticky + Pokročilé nastavenia + Spustiť po zapnutí zariadenia + Udržiavať aplikáciu aktívnu pomocou Služby + ID zariadenia + Iba alfanumerické znaky + URL adresa servera pre zdieľanie súborov + URL adresa servera pre zdieľanie záznamov + Bude používaný proprietárny formát súboru + Šifrovanie médií + Povinné šifrovanie médií + Vytvoriť koncové šifrované schôdzky a skupinové hovory + Prijímať zvuk pred spojením hovoru (early media) + Prenášať zvuk pri odchádzajúcom hovore (early media) + Automaticky prijímať prichádzajúce hovory + Oneskorenie v milisekundách + URL pre vzdialenú správu + Stiahnuť a použiť + Zvukové zariadenia + Zvukové kodeky + mono + stereo + Video kodeky + &appName; Nastavenia Androidu + Vývojárske nastavenia + Zobraziť vývojárske nastavenia + Vývojárske nastavenia povolené + Vývojárske nastavenia sú už povolené + Podrobnosti + Zariadenia + Zariadenie nebolo nájdené… + Pridať obrázok + Upraviť obrázok + Odstrániť obrázok + Účet bol zakázaný, nebudete môcť prijímať hovory ani správy. + Účet sa pripája k serveru, prosím, čakajte… + Pripojenie účtu zlyhalo, skontrolujte nastavenia. + Medzinárodný prefix + Nastavenia účtu + Odhlásiť sa + Vybrať režim účtu + Použiť + Režim s koncovým šifrovaním + Režim vzájomnej kompatibility + Tento režim Vám umožňuje využívať všetky funkcie &appName; a zároveň zostať kompatibilný s akoukoľvek SIP službou vďaka šifrovaniu medzi dvomi koncovými bodmi. + Odstrániť + Posledné pripojenie: + Odhlásiť sa z Vášho účtu? + Pokiaľ si želáte nenávratne zmazať svoj účet, navštívte: https://sip.linphone.org + Nastavenia účtu + Povoliť push notifikácie + Šifrovanie správ je povinné + URL adresa SIP proxy servera + Nastavenia zásad NAT + URL adresa servera STUN/TURN + Povoliť ICE + TURN používateľské meno + TURN heslo + AVPF (Profil audio-vizuálu so spätnou väzbou) + Platnosť (v sekundách) + URI adresa pre audio/video hovory + RL adresa servera CCMP (Cisco CallManager Provisioning) + URL adresa servera pre kľúče koncového šifrovania + Režim zoskupenia + URI adresa hlasovej schránky + URI adresa servera MWI (Message Waiting Indicator) + Formátovať telefónne čísla s medzinárodnou predvoľbou + Aktualizovať heslo + Vyžaduje sa overenie + Heslo + Nepodarilo sa nájsť zodpovedajúci účet! + Nový hovor + Hľadať kontakt alebo históriu hovoru + Vytvoriť skupinový hovor + Momentálne žiadny návrh ani kontakt… + Nastaviť predmet skupinového hovoru + Predmet skupinového hovoru + Momentálne žiadny hovor… + Konverzácia + Naozaj chcete zmazať celú históriu hovorov? + Z histórie budú odstránené všetky hovory + Naozaj chcete vymazať históriu s touto osobou? + Z histórie budú odstránené všetky hovory + Momentálne žiadny kontakt… + Zmeniť filter + Obľúbené + Konverzácie + Nahrávanie zvuku: Aby Vás osoba na druhej strane mohla počuť a aby ste mohli nahrávať hlasové správy. + Zapisovať záznamy do logcatu + Chyba pri nahrávaní ladiacich záznamov + Vyberte svoju krajinu, aby &appName; mohol správne spárovať Vaše kontakty. + Povoliť TURN + Kontakty + Žiadny účet zatiaľ nie je nastavený + Tento účet je aktívny, každý Vám môže volať. + Tento režim zaručuje dôvernosť Vašich údajov. Naša technológia koncového šifrovania poskytuje najvyššiu úroveň bezpečnosti pre Vaše komunikácie. + Počas kontroly aktualizácie nastala chyba + Autentifikačná doména + Spravovať účet + Aké informácie &appName; zbiera a používa + Kontrola aktualizácie + Prispieť do prekladu &appName; + Po aktivácii bude potrebné aplikáciu reštartovať.\nPo reštarte budú všetky dáta aplikácie zašifrované a prístupné iba cez túto aplikáciu.\n\nBuďte opatrný, tento krok je nenávratný! + Nepodarilo sa povoliť modul šifrovania! + Adaptívny kontrola rýchlosti + Vibrovať počas prichádzajúceho hovoru + Sprístupniť zverejnenie stiahnutých médií + Hostiteľ + Pomôžte vývojárom pri riešení problémov odosielaním záznamov do služby Crashlytics po páde aplikácie + Vyzváňať počas prichádzajúceho hovoru (early media) + Predvolené vstupné zvukové zariadenie + Predvolené výstupné zvukové zariadenie + Momentálne žiadny SIP kontakt… + Vyplňte, prosím, aspoň zobrazované meno a URL adresu servera + Oneskorenie medzi dvomi dotazmi (v milisekundách) + Predvolené rozloženie + Všetky kontakty + Používať iba siete Wi-Fi + Proxy server pre odchádzajúcu komunikáciu + Použiť CPIM v \"základných\" konverzáciách + Pri formátovaní telefónnych čísiel nahradiť \"+\" za \"00\" + Pripojenie sa nepodarilo, pretože chýba alebo je neplatné overenie účtu\n%s.\n\nPokúste sa znovu zadať heslo alebo skontrolovať konfiguráciu účtu v nastaveniach. + Nahrávať videohovory pomocou H265/AV1 + Oneskorenie pred automatickým prijatím hovoru + Push notifikácie nie sú dostupné! + URI adresa pre vytváranie konferencií + Zobraziť všetko + Zobraziť &appName; kontakty + Zobraziť SIP kontakty + Nový kontakt + Upraviť kontakt + Priezvisko + Spoločnosť + Pracovná pozícia + Zmeny boli úspešne uložené + Uloženie zmien zlyhalo! + Vytvorenie kontaktu zlyhalo! + Neuložiť zmeny? + Všetky zmeny budú stratené + Vyplňte, prosím, buď krstné meno, priezvisko alebo spoločnosť + Telefónne čísla a SIP adresy + Spoločnosť: + Zariadenie sa nenašlo… + Počet dôveryhodných zariadení: + Dôvera + Ďalšie akcie + Upraviť + Pridať do obľúbených + Zdieľať + Vymazať + Ahoj, pridaj sa ku mne na &appName;! Aplikáciu si môžeš zdarma stiahnuť tu: %s + Kontakt bol odstránený + Zvýšiť úroveň dôvery + Chystáte sa volať na zariadenie %2$s používateľa %1$s.\nŽeláte si hovor uskutočniť? + Úroveň dôvery + Vymazať %s? + Tento kontakt bude definitívne odstránený. + Vyberte číslo alebo SIP adresu + Dostupný na %s + Dostupný dnes v %s + Dostupný včera v %s + Nedostupný + Dostupný + Nerušiť + Volať + Volať + Správa + Overiť + Nepomenované zariadenie + Momentálne žiadna konverzácia… + Prebieha odstraňovanie… + %s: + Text čaká na zdieľanie + Označiť ako prečítané + Stlmiť + Zrušiť stlmenie + Vymazať konverzáciu + Opustiť skupinu + Nastavenie dočasných (miznúcich) správ + Dočasné (miznúce) správy + Videohovor + Kontakt bol úspešne vytvorený + Odstrániť z obľúbených + Číslo bolo skopírované do schránky + Vymazať + Krstné meno + Pracovná pozícia: + Skontrolujte všetky zariadenia svojich kontaktov, aby ste mali istotu, že Vaše komunikácie budú zabezpečené a nepozmenené. \nKeď budú všetky overené, dosiahnete maximálnu úroveň dôvery. + + %s súbor čaká na zdieľanie + %s súbory čakajú na zdieľanie + %s súborov čaká na zdieľanie + + Nové správy sa automaticky odstránia, keď si ich všetci prečítajú.\nVyberte dobu trvania: + Zakázané + 1 minúta + 1 hodina + 1 deň + 3 dni + 1 týždeň + Nová konverzácia + Nová skupinová konverzácia + Prosím, zadajte názov konverzácie + Hľadať kontakt + Vytvoriť skupinovú konverzáciu + Momentálne žiadny kontakt ani návrh… + Žiadny zhodný výsledok… + Napíšte niečo… + + %s píše… + %s píšu… + %s píše… + + Pridať účastníkov + Odpovedá na: + Hľadať + Informácia o konverzácii + Dočasné (miznúce) správy + Médiá + Dokumenty + Žiadne médiá nenájdené… + Žiadne dokumenty nenájdené… + Koncové šifrovanie konverzácie + Správy v tejto konverzácii sú koncovo šifrované. Dešifrovať ich môže len váš partner v konverzácii. + Táto konverzácia nie je šifrovaná! + Táto konverzácia bola z bezpečnostných dôvodov deaktivovaná. + Dosiahnutý maximálny možný počet príloh! + Nastaviť predmet konverzácie + Upraviť predmet konverzácie + Predmet je povinný + Predmet konverzácie + Otvoriť alebo exportovať súbor? + &appName; nemôže tento súbor otvoriť.\n\nMienite ho otvoriť v inej aplikácii (ak je to podporované), alebo ho exportovať do Vášho zariadenia? + Otvoriť súbor + Exportovať súbor + Otvoriť ako textový súbor? + Otvoriť ako textový súbor + Nahrávka nemôže byť prehraná! + Správa bola vymazaná + Vytvorenie konverzácie zlyhalo! + Vytvorenie konverzácie s účastníkom z inej domény nie je z bezpečnostných dôvodov povolené! + Vybrané médiá neboli nájdené + Predmet konverzácie bol zmenený + Dočasné (miznúce) správy boli povolené + Dočasné (miznúce) správy boli zakázané + Dosiahnutá maximálna možná dĺžka trvania + Pridanie účastníka(-ov) do konverzácie zlyhalo + Konverzácia bola úspešne vymazaná + Opustili ste skupinu + Konverzácia nebola nájdená + Nenájdený žiadny zodpovedajúci výsledok + Bol dosiahnutý posledný zodpovedajúci výsledok + Odfotiť + Dočasné (miznúce) správy boli zakázané + Otvoriť galériu + Vybrať súbor + Súbor sa nedá otvoriť! + Členovia skupiny (%s) + Pridať účastníkov + Administrátor + Vymazať históriu + Odstrániť zo skupiny + Udeliť práva administrátora + Odobrať práva administrátora + Zobraziť profil kontaktu + Pridať do kontaktov + Z histórie budú odstránené všetky správy + História bola úspešne vymazaná + %s sa pripojil do konverzácie + %s opustil konverzáciu + %s už nie je administrátorom + Kontakt nebol nájdený + Nenájdená žiadna adresa na uloženie do kontaktu + Zahájiť skupinový hovor? + Bude volané všetkým účastníkom. + Pridali ste sa ku skupine + Opustili ste skupinu + %s sa pripojil + nové zariadenie pre %s + zariadenie pre %s bolo odstránené + nový predmet: %s + %s je administrátor + %s už nie je administrátorom + Identifikačný kľúč LIME sa zmenil pre %s + Zistený útok typu MIM (man-in-the-middle) pre %s + Úroveň bezpečnosti bola znížená kvôli %s + Médiá a dokumenty + Zdieľané médiá + Zaručená dôvernosť + Vďaka technológii koncového šifrovania v &appName; je zachovaná dôvernosť správ, hovorov a schôdzok. Vymieňané údaje nemôže dešifrovať nikto, ani my sami. + Skutočne chcete vymazať všetky správy? + %s je odteraz administrátorom + Nenájdená žiadna aplikácia na otvorenie tohto typu súboru + Maximálny počet účastníkov bol prekročený o %s + Nenájdená žiadna aplikácia na otvorenie tohto typu súboru.\n\nChcete ho otvoriť ako textový súbor? + %s sa odpojil + Doba platnosti dočasných (miznúcich) správ je teraz %s + Dočasné (miznúce) správy boli povolené + Doba platnosti dočasných (miznúcich) správ bola zmenená + Zdieľané dokumenty + Preposlať správu… + Správa bola preposlaná + Prečítaná %s + Doručená %s + Odoslaná %s + Chyba %s + Reakcie %s + %1$s %2$s + Kliknutím odstránite + Preposlané + pozvánka na schôdzku: + schôdzka zrušená: + hlasová správa + Nadnes nie je naplánovaná žiadna schôdzka + Nová schôdzka + Schôdzka + Hromadné odosielanie + Pridať názov… + Zvoľte dátum začiatku + Zvoľte čas začiatku + Zvoľte čas konca + Časová zóna + Jednorazovo + Pridať popis + Pridať účastníkov + Kliknutím pridať ďalších účastníkov + Pridať hovoriaceho (rečníka) + Pripojiť sa k schôdzke teraz + Organizátor + Vytvoriť udalosť v kalendári + Schôdzka bola vymazaná + Schôdzka nebola nájdená! + Popis + Upraviť schôdzku + Zrušiť schôdzku + Vymazať schôdzku? + Želáte si vymazať schôdzku? + Vymazať schôdzku + Schôdzka bola aktualizovaná + Schôdzka bola zrušená + Schôdzka zrušená + Úprava schôdzky zlyhala! + Vyplňte názov a vyberte aspoň jedného účastníka + Odoslanie pozvánok na schôdzku niektorým účastníkom zlyhalo! + Adresa schôdzky skopírovaná do schránky + Pripojiť sa + Zrušiť + Prebieha pripojenie + Čoskoro sa pripojíte + Pripojenie k schôdzke zlyhalo! + Odchádzajúci hovor + Prichádzajúci hovor + Ukončili ste hovor + Protistrana ukončila hovor + Prichádzajúci hovor pre %s + Prichádzajúci videohovor pre %s + Presmerovanie %s na… + Žiadny ďalší hovor + Potvrdiť presmerovanie hovoru + Chystáte sa presmerovať hovor %1$s na %2$s. + Presmerovať + Nový hovor + Zoznam hovorov + Číselník (numerická klávesnica) + Správy + Obnoviť + Nahrať + Zavesiť + Rozloženie + Prebieha + Vyzváňa + Prichádzajúci + Aktívny + Pozastavené + Obnovujem… + Ukončené + Čaká sa na šifrovanie… + Koncové šifrovanie pomocou ZRTP + Znovu overiť ZRTP SAS + Vyžadované overenie + Hovor nie je šifrovaný + Zoznam hovorov + Hovor sa nahráva + %s nahráva + %s hovorov + %s pozastavené hovory + Zlúčiť všetky hovory do konferencie? + Vytvoriť konferenciu + Oprávnenie pre prístup ku kamere bolo zamietnuté! + Overiť zariadenie + Pre vašu bezpečnosť musíme znovu overiť zariadenie Vašek protistrany.\nProsím, znovu si vymeňte svoje kódy: + Váš kód: + Kód protistrany: + Žiadna zhoda + Bezpečnostné upozornenie + Skúsiť znovu + Dôvernosť tohto hovoru môže byť ohrozená! + Slúchadlo + Reproduktor + Bluetooth (%s) + Naslúchadlo (%s) + Náhlavná súprava + Slúchadlá + Zvuk + Kodek: %s + Miera straty: %s + Pozastavené vzdialenou stranou + Preposlanie správy bolo zrušené + Odoslať pozvánku účastníkom + Šírka pásma: %s + Pozastaviť + Schôdzka bola vytvorená + Naplánovanie schôdzky zlyhalo! + Koncové šifrovanie pomocou SRTP + schôdzka aktualizovaná: + Informácia o hromadnom odosielaní.\nZistiť viac + Oprávnenie k nahrávaniu zvuku bolo zamietnuté! + Pre vašu bezpečnosť musíme overiť zariadenie Vašej protistrany.\nProsím, vymeňte si svoje kódy: + Odoslanie všetkých pozvánok na schôdzku zlyhalo! + Aktuálne hovory + Prichádzajúci videohovor + Koncovo šifrované pomocou ZRTP + Vyrovnávacia pamäť (jitter buffer): %s + Video (obraz) + Rozlíšenie: %s + FPS: %s + FEC + Stratené pakety: %s + Opravené pakety: %s + Šírka pásma: %s + Šifrovanie médií + Šifrovanie médií: %s + Post-kvantové ZRTP + Šifrovací algoritmus: %s + Hašovací algoritmus: %s + Autentifikačný algoritmus: %s + SAS algoritmus: %s + História bola vymazaná + Zariadenie overené + Používateľ je zaneprázdnený + Používateľ nebol nájdený + Nekompatibilné parametre médií + Hovor bol úspešne presmerovaný + Presmerovanie hovoru zlyhalo! + Dočasne nedostupné + Zdieľať pozvánku + Čakanie na ďalších účastníkov… + Zdieľať obrazovku + Účastníci + Zlúčenie hovoru zlyhalo! + Odstrániť %s z konferencie? + Účastník bol odstránený z konferencie + Pripojovanie… + Pozastavené + zdieľa svoju obrazovku + Neplatná SIP adresa, nedá sa pridať do konferencie + Mozaika + Hovoriaci (rečník) + Príliš veľa účastníkov pre mozaikové rozloženie + Vzdialený skupinový hovor + Miestny skupinový hovor + Nahrávky + Momentálne žiadne nahrávky… + Zatiaľ žiadne obľúbené kontakty + Pridať do kontaktov + Zobraziť kontakt + Kopírovať SIP adresu + Kopírovať telefónne číslo + Vymazať históriu + Pozvať + Znovu odoslať + Stav doručenia + Odpovedať + Kopírovať + Stiahnuť + Zdieľať + Oranžová + Žltá + Zelená + Modrá + Červená + Ružová + Fialová + Žiadne výsledky… + Tu sa zobrazia vybraní účastníci + Chyba pripojenia účtu(-ov) + Zvolený účet je momentálne zakázaný + Nie ste pripojený k internetu + Zapnutý režim len cez Wi-Fi + Prebieha operácia, prosím, čakajte + Obľúbené + Návrhy + Povolenie na zobrazovanie prichádzajúcich hovorov nebolo udelené! + + %s nová hlasová správa + %s nové hlasové správy + %s nových hlasových správ + + Preskočiť + Preskočiť + Schôdzka bola zrušená! + Kontakt je dôveryhodný + Kontakt nie je dôveryhodný! + Kontakt je dostupný + Kontakt nie je dostupný + Otvoriť menu + Vrátiť sa späť + Zavrieť oznámenie + Uložiť zmeny + Zobraziť menu + Kliknutím zobraziť viac informácií + Kliknutím odstrániť účastníka + Zobraziť alebo skryť heslo + Rozbalí/zbalí spodný panel + Ukončí hovor + Prijme hovor + Prijme videohovor + Spustí hovor + Spustí videohovor + Povolí/zakáže odoslanie obrazu z kamery + Zmení výstupné zvukové zariadenie + Zmení používanú kameru (prednú/ zadnú) + Hovor je pozastavený + Zobrazí štatistiky hovoru + Tento hovor je nahrávaný + Zatvorí filter + Vyčistí aktuálny filter + Vytvorí skupinovú konverzáciu + Spustí skupinový hovor + Kliknutím zobrazíte všetky dostupné možnosti + Účastník je stlmený + Pridať účastníkov + Spustí/zastaví prehrávanie videa + Zdieľať súbor + Uložiť súbor + Obrázok priložený k správe + Video priložené k správe + Súbor priložený k správe + Táto správa bola preposlaná z inej konverzácie + Kliknutím zobrazíte stav doručenia + Spustí/zastaví prehrávanie hlasovej správy + Odstrániť tento súbor z príloh + Zavrieť prílohy + Dočasné (miznúce) správy sú povolené + Zavrieť zoznam účastníkov + Zruší nahrávanie hlasovej správy + Zastaví nahrávanie hlasovej správy + Spustí nahrávanie hlasovej správy + Odošle správu v konverzácii + Otvorí výber emoji + Otvorí výber súboru + Zapne/vypne stlmenie konverzácie + Konverzácia sa odstraňuje + Táto konverzácia nie je zabezpečená + Hľadať smerom hore + Hľadať smerom dole + Začať novú konverzáciu + Prejsť na dnešok + Naplánovať schôdzku + Naplánovať túto schôdzku + Upraviť schôdzku + Zdieľať adresu schôdzky + Zoznam účastníkov + Zariadenie je dôveryhodné + Upraviť kontakt + Odstrániť pole + Zobraziť filter zoznamu kontaktov + Pripojiť sa ku konferencii + Uložiť konfiguráciu CardDAV + Vymazať túto konfiguráciu LDAP + Uložiť konfiguráciu LDAP + Prehrá záznam hovoru + Ísť na konverzáciu + Kopírovať text do schránky + Hlasová správa je dostupná + Kliknutím zrušíte súbory alebo text čakajúci na zdieľanie + Potvrdiť nový zoznam účastníkov + Vypne/zapne mikrofón + Zlúčenie hovorov do konferencie + Hovor je presmerovaný + + Účastník (%s) + Účastníci (%s) + Účastníkov (%s) + + Zobrazí filter + Iba zvuk + Obľúbené + + %s oznámenie pre ďalší účet(ďalšie účty) + %s oznámenia pre ďalší účet(ďalšie účty) + %s oznámení pre ďalší účet(ďalšie účty) + + Preposlať + Odstráni poslednú číslicu alebo znak + Zobrazí číselník (numerickú klávesnicu) + Účastník hovorí + Algoritmus výmenu kľúča: %s + Povolenie na zasielanie notifikácií nebolo udelené! + Zabudnuté heslo? + Spustí/zastaví prehrávanie zvuku + Vytvorenie skupinového hovoru zlyhalo! + Server neodpovedá, skúste to znova + Službe nie je dostupná alebo nastala chyba siete + Naozaj chceš tohto účastníka odstrániť z konferencie? + Táto správa je odpoveďou na predchádzajúcu správu + Schôdzka bola aktualizovaná + Konverzácia bola stíšená + Posunie sa na prvú neprečítanú správu alebo na koniec + Správa už ďalej nebude odpoveďou na predchádzajúcu správu + Vymazať túto konfiguráciu CardDAV + Kliknutím upraviť názov konverzácie + Vytvoriť kontakt + Podržaním tlačidla volanie hlasovej schránky + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml new file mode 100644 index 0000000000..2c905b1068 --- /dev/null +++ b/app/src/main/res/values-uk/strings.xml @@ -0,0 +1,856 @@ + + +]> + + SIP-адреса + ID пристрою + Ім\'я для відображення + Домен + Ім\'я користувача + Ідентифікатор автентифікації (якщо відрізняється) + Пароль + Номер телефону + або + Наступний + Старт + Сьогодні + Вчора + в &appName; + Захищений + Ваше спілкування убезпечене завдяки нашому наскрізному шифруванню. + Відкритий код + Новий обліковий запис налаштовано + Файл завантажено до документів + Помилка під час спроби завантаження файлу до документів + Конфігурацію успішно застосовано + Помилка під час спроби створити медіаплеєр + Загальні умови та політика конфіденційності + загальні умови + політика конфіденційності + Файл завантажено до галереї + Ласкаво просимо + Безкоштовний застосунок з відкритим кодом з 2001 року. + SIP-адресу скопійовано в буфер обміну + Помилка під час спроби завантажити файл до галереї + + %s день + %s дні + %s днів + %s днів + + + %s обрано + + + + + Відмовити + Прийняти + Скасувати + Продовжити + Виклик + Видалити + Встановити + Більше не показувати це діалогове вікно + Ні + Так + Видалити + Підтвердити + Повідомлення про активні виклики &appName; + Повідомлення про вхідні дзвінки &appName; + Повідомлення про пропущені дзвінки &appName; + Сервірсне повідомлення &appName; + Цей сервіс працюватиме постійно, щоб підтримувати активність програми та дозволяти вам отримувати дзвінки та повідомлення без push-сповіщень. + Сповіщення про миттєві повідомлення &appName; + Відреагував %1$s на: %2$s + Позначити як прочитане + Відповісти + Пропущено виклик від %s + Пропущено груповий виклик: %s + %s пропущено викликів + Пропущений виклик + &appName; + Пошук нових повідомлень + &appName; + Триває передача файлів + + %s файл вивантажується + %s файли вивантажуються + %s файлів вивантажуються + %s файлів вивантажуються + + + %s файл завантажується + %s файли завантажується + %s файлів завантажується + %s файлів завантажується + + Натисніть, щоб відкрити + Захищений, з відкритим вихідним кодом та французький застосунок для спілкування. + Гучність занадто низька, можливо, ви нічого не чуєте! + Помилка під час спроби завантажити та застосувати віддалену конфігурацію + Продовжуючи, Ви приймаєте наші %1$s та %2$s. + Підтвердьте номер телефону + Ви впевнені, що ваш номер телефону %s? + Логін + Відскануйте QR-код + Недійсний QR-код! + Єдиний вхід + SIP-адреса недійсна! + SIP-адреса не містить імені користувача! + Обліковий запис вже існує + Зареєструватися + Підтвердьте свій номер телефону + Невірний номер? + Створити + Створіть обліковий запис, використовуючи свою електронну адресу: + Вже маєте обліковий запис? + Транспорт + Я надаю перевагу створенню облікового запису &appName; + Я розумію + Схоже, push-сповіщення недоступні на вашому пристрої, але вони є обов’язковими для створення облікового запису в мобільному застосунку.\n\nЗапрошуємо вас створити обліковий запис на нашій веб-платформі: + Сталася неочікувана помилка, спробуйте ще раз пізніше + Невірне ім\'я користувача або пароль + Не вдалося залогінитись: код помилки %s + Надати дозволи + ОК + Зробіть це пізніше + Забули пароль? + Оберіть, як відновити свій обліковий запис. + Ви створили свій обліковий запис, використовуючи: + Електронна пошта + Номер телефону + Контакти + Дзвінки + Розмови + Наради + Керування профілем + Підключено + Оновлюється + Вимкнено + Підключення… + Додати обліковий запис + Допомога + Про &appName; + Політика конфіденційності + Версія + Перевірити оновлення + Розширений + Ваша версія актуальна + Доступне оновлення + Доступна нова версія %s. Бажаєте оновити? + Закрити застосунок + Усунення несправностей + Друкувати логи в logcat + Видалити логи + Push-сповіщення з токеном авторизації не отримано упродовж 5 секунд, спробуйте пізніше + Сторонній обліковий запис SIP + Ми надіслали код підтвердження на ваш номер телефону %1$s.\n\nБудь ласка, введіть код підтвердження нижче: + Використати сторонній обліковий запис SIP + Ще немає облікового запису? + Для деяких функцій потрібен обліковий запис &appName;, такі як груповий обмін повідомленнями, відеоконференції…\n\nЦі функції приховані, якщо ви реєструєтесь за допомогою стороннього облікового запису SIP.\n\nЩоб увімкнути їх у комерційному проєкті, зв’яжіться з нами. + Щоб повною мірою користуватися &appName;, вам потрібно надати застосунку наступні дозволи: + Схоже, push-сповіщення недоступні на вашому пристрої, але вони обов’язкові для відновлення облікового запису номера телефону в мобільному застосунку. + Помилка + Обліковий запис ще не налаштовано + Яку інформацію збирає та використовує &appName; + Зробіть свій внесок у переклад &appName; + Під час перевірки оновлень сталася помилка + Обрати мелодію дзвінка + Зробити завантажені медіафайли загальнодоступними + Редагувати адресну книгу CardDAV + Без пропозицій та контактів на цей час… + Темна тема + Порт + Використовувати два сервери + Авто + URI сервера MWI (індикатор очікування повідомлення) + Зелений + Червоний + Рожевий + Фіолетовий + Обрані учасники з\'являться тут + + %s нове голосове повідомлення + %s нових голосових повідомлень + %s нових голосових повідомлень + %s нових голосових повідомлень + + Розмова видаляється + Основний колір + Контакт в мережі + Відобразити меню + Натисніть, щоб видалити учасника + Допоможіть розробникам вирішувати проблеми, надсилаючи логи до Crashlytics після збою + URL-адреса сервера обміну файлами + URL-адреса сервера обміну логами + ділитись своїм екраном + Запросити + Статус доставки + Буде використано власний формат файлу + Відповісти + Дзвінок під час вхідного раннього медіадзвінка + Не вдалося підключитися до облікового запису, перевірте налаштування. + Відповісти на відеодзвінок + Затримка перед автоматичною відповіддю на дзвінок + Міжнародний префікс + Вихідний аудіопристрій за замовчуванням + Налаштування розробника ввімкнено + Редагувати контакт + Будь ласка, введіть ім\'я, прізвище або назву компанії + Контакт було видалено + Увімкнути/вимкнути надсилання зображення з камери + Редагувати зображення + + %s файл очікує завантаження + %s файли очікують завантаження + %s файлів очікують завантаження + %s файлів очікують завантаження + + Наразі жодного контакту та жодних пропозицій… + Ви збираєтесь перенаправити дзвінок %1$s на %2$s. + Показує статистику дзвінку + Відео додано до повідомлення + Переслати + Копіювати + Поділитись + Помаранчевий + Жовтий + Цей обліковий запис онлайн, кожен може вам зателефонувати. + Не знайдено… + Виберіть режим облікового запису + Цей режим гарантує конфіденційність ваших даних. Наша технологія наскрізного шифрування забезпечує найвищий рівень безпеки ваших комунікацій. + Будь ласка, введіть назву для розмови + Закриває фільтр + Цей режим дозволяє вам користуватися всіма функціями &appName;, залишаючись сумісним з будь-яким SIP-сервісом завдяки шифруванню \"точка-точка\". + Усі дзвінки будуть видалені з історії + Ви збираєтеся зателефонувати на пристрій %2$s користувача %1$s.\nВи хочете здійснити дзвінок? + Увімкнути TURN + URI ресурсу аудіо/відео конференцій + Створити груповий дзвінок + Номер скопійовано в буфер обміну + Режим «Тільки Wi-Fi» увімкнено + Заміна + на 00 під час форматування номерів телефонів + Тимчасові повідомлення + Повідомлення в цій розмові зашифровані за методом електронного шифрування (e2e). Розшифрувати їх може лише ваш співрозмовник. + Показує цифрову клавіатуру + Учасник говорить + Ви справді хочете видалити всю історію дзвінків? + Ви дійсно хочете видалити всі повідомлення? + Виявлено атаку типу «людина посередині» для %s + Повідомлення було перенаправлено + Не вдалося зберегти зміни! + Видалити з обраних + Починає/призупиняє відтворення аудіо + Не знайдено жодної програми для відкриття цього типу файлу.\n\nСпробувати відкрити його як звичайний текстовий файл? + Не знайдено програми для відкриття цього типу файлу + Читати контакти: Для відображення своїх контактів та знайти тих, хто користується &appName;. + Сповіщення про публікації:Щоб бути в курсі отримання повідомлення або дзвінка. + Запис аудіо: Щоб ваш співрозмовник міг вас чути та записувати голосові повідомлення. + Доступ до камери: Для запису відео під час відеодзвінків та конференцій. + Контакти + Привіт, приєднуйтесь до мене на &appName;! Ви можете завантажити його безкоштовно за адресою %s + Обрані + Перевірте всі свої контактні пристрої, щоб переконатися, що ваші повідомлення будуть захищені та незмінні.\nКоли все буде перевірено, ви досягнете максимального рівня довіри. + Оберіть час початку + Нові повідомлення будуть автоматично видалені після того, як їх усі прочитають.\nОберіть тривалість відображення повідомлення: + Термін дії тимчасових повідомлень змінено + Видалити з групи + Поділитися логами + Версія застосунку + Версія SDK + ID проекта Firebase + Поділитись посиланням на логи налагодження використовуючи… + Журнали налагодження очищено + Не вдалося завантажити логи налагодження + Показати конфігурацію + Налаштування + Безпека + Зашифрувати все + Увага: після ввімкнення вимкнути неможливо! + Не вдалося ввімкнути модуль шифрування! + Модуль шифрування ввімкнено + Калібрування апаратного придушувача луни + виконується + луна відсутня + %s мс + Калібрування не вдалося + Адаптивне керування швидкістю + Увімкнути відео + Увімкнути FEC відео + Вібрація під час вхідного дзвінка + Автоматично починати запис дзвінків + Змінити мелодію дзвінка + Розмови + Автоматичне завантаження файлів + Позначати розмову як прочитану під час закриття сповіщення про повідомлення + Контакти + Додати LDAP сервер + Редагувати LDAP сервер + Після активації вам доведеться перезапустити застосунок.\nПісля цього всі дані застосунку будуть зашифровані та доступні лише через застосунок.\n\nБудьте обережні, це не можна скасувати! + Запобігання запису інтерфейсу застосунку + Дзвінки + Використовуйте програмне придушення луни + Усуває появу луни, яка може бути чутна з іншого боку, якщо апаратне придушення луни недоступне + Додати адресну книгу CardDAV + Ім\'я для відображення + URL сервера + Ім\'я користувача + Пароль + Аутентифікація + Зберігайте в ньому щойно створені контакти + Синхронізація пройшла вдало + Помилка синхронізації! + Це повідомлення було переслано з іншої розмови + Обліковий запис CardDAV видалено + Будь ласка, вкажіть принаймні ім\'я користувача та URL-адресу сервера + URL-адреса сервера (не може бути порожньою) + Прив\'язати DN + Пароль + Використовувати TLS + База пошуку (не може бути порожньою) + Фільтр + Максимальна кількість для відображення + Тайм-аут (у секундах) + Затримка між двома запитами (у мілісекундах) + Мінімальна кількість символів для запиту + Атрибути назви + Атрибути SIP + SIP-домен + Сталася помилка, LDAP-сервер не збережено! + Наради + Відображати за замовчуванням + Активний промовець + Мозаїка + Мережа + Видалити цей файл із вкладень + Використовувати лише Wi-Fi мережу + Дозволити IPv6 + Інтерфейс користувача + Автоматично відкривати панель набору номера + Тема + Прокручує до першого непрочитаного повідомлення або до кінця + Повідомлення більше не буде відповіддю на попереднє повідомлення + Запланувати нараду + Світла тема + Тунель + Сервер + Другий сервер + Другий порт + Режим + Вимкнено + Завжди + Авто + Розширені налаштування + Запускати під час завантаження пристрою + Підтримуйте активність застосунку за допомогою Сервісу + ID пристрою + Тільки буквено-цифрові символи + Запис відеодзвінків за допомогою H265/AV1 + Шифрування медіа + Обов\'язкове шифрування медіафайлів + Створювати наскрізні зашифровані зустрічі та групові дзвінки + Приймати ранні медіадані + Дозволити вихідні ранні медіафайли + Автоматична відповідь на вхідні дзвінки + Затримка в мілісекундах + URL-адреса віддаленого налаштування + Завантажити та застосувати + Аудіопристрої + Вхідний аудіопристрій за замовчуванням + Аудіокодеки + моно + стерео + Відеокодеки + &appName; налаштування Android + Налаштування розробника + Показати налаштування розробника + Налаштування розробника вже ввімкнено + Керування обліковим записом + Подробиці + Пристрої + Пристрій не знайдено… + Додати зображення + Видалити зображення + Обліковий запис вимкнуто, ви не отримуватимете вхідних дзвінків чи повідомлень. + Обліковий запис підключається до сервера, будь ласка, зачекайте… + Учаснику вимкнено звук + Оберіть свою країну, щоб &appName; міг зіставляти ваші контакти. + Налаштування облікового запису + Вийти + Застосувати + Режим наскрізного шифрування + Взаємосумісний режим + Видалити + Останнє з\'єднання: + Вийти з облікового запису? + Якщо ви хочете видалити свій обліковий запис назавжди, перейдіть за посиланням: https://sip.linphone.org + Налаштування облікового запису + Дозволити push-сповіщення + Push-сповіщення недоступні! + Обов\'язкове шифрування миттєвих повідомлень + URL-адреса проксі-сервера SIP + Вихідний проксі-сервер + Налаштування політики NAT + URL-сервера STUN/TURN + Увімкнути ICE + Ім\'я користувача TURN + Пароль TURN + AVPF + Спливає (в секундах) + URI ресурсу конференцій + URL-адреса сервера CCMP + URL-адреса сервера ключів шифрування E2E + Режим об\'єднання + Використовуйте CPIM у «базових» розмовах + URI голосової пошти + Форматування номерів телефонів з використанням міжнародного префікса + Оновити пароль + Потрібна автентифікація + З’єднання не вдалося, оскільки автентифікація відсутня або недійсна для облікового запису\n%s.\n\nВи можете ввести пароль ще раз або перевірити конфігурацію облікового запису в налаштуваннях. + Пароль + Не вдалося знайти відповідний обліковий запис! + Новий дзвінок + Пошук контакту або історії дзвінків + Встановити назву групового дзвінка + Тема групового дзвінка + Наразі дзвінка немає… + Розмова + Усі дзвінки будуть видалені з історії + Ви справді хочете видалити історію спілкування з цим контактом? + Наразі контакти відсутні… + Наразі SIP-контакти відсутні… + Змінити фільтр + Обрані контакти + Усі контакти + Переглянути усі + Переглянути контакти &appName; + Переглянути SIP-контакти + Новий контакт + Ім\'я + Прізвище + Компанія + Посада + Зміни успішно збережено + Контакт успішно створено + Не вдалося створити контакт! + Не зберігати зміни? + Усі зміни будуть втрачені + Номери телефонів та SIP-адреси + Компанія: + Посада: + Довіряти + Пристрій не знайдено… + Кількість довірених пристроїв: + Інші дії + Редагувати + Додати в обрані + Поділитися + Видалити + Підвищити рівень довіри + Рівень довіри + Видалити %s? + Цей контакт буде остаточно видалено. + Виберіть номер або SIP-адресу + В мережі + В мережі об %s + В мережі сьогодні об %s + В мережі учора об %s + Не доступний + Не турбувати + Дзвінок + Повідомлення + Відеодзвінок + Перевірити + Безіменний пристрій + Наразі жодної розмови… + Триває видалення… + %s: + Текст очікує на поширення + Позначити як прочитане + Вимкнути звук + Увімкнути звук + Дзвінок + Видалити розмову + Вийти з групи + Налаштування тимчасових повідомлень + Тимчасові повідомлення + Вимкнено + 1 хвилина + 1 година + 1 доба + 3 доби + 1 тиждень + Нова розмова + Нова групова розмова + Пошук контакту + Створити групову розмову + Відповідних результатів немає… + Скажіть щось… + + %s скомпоновано + + + + + Додати учасників + Відповісти на: + Пошук + Інформація про розмову + Медіа + Документи + Медіафайли не знайдено… + Документи не знайдено… + Наскрізне шифрування розмови + Гарантована конфіденційність + Ця розмова не зашифрована! + Заради вашої безпеки цю розмову було вимкнено. + Досягнуто максимальної кількості вкладень! + Встановити назву розмови + Редагувати назву розмови + Назва обов\'язкова + Назва розмови + Відкрити чи експортувати файл? + &appName; не може відкрити цей файл.\n\nБажаєте відкрити його в іншій програмі (якщо можливо) чи експортувати на свій пристрій? + Відкрити файл + Завантажити файл + Відкрити як звичайний текстовий файл? + Відкритий текстовий файл + Голосовий запис не можна відтворити! + Повідомлення було видалено + Обрані медіафайли не знайдено + Назву розмови було змінено + Тимчасові повідомлення увімкнуто + Тимчасові повідомлення вимкнуто + Максимальна тривалість досягнута + Не вдалося додати учасника(ів) до розмови + Розмову успішно видалено + Ви вийшли з групи + Розмову не знайдено + Відповідного результату не знайдено + Досягнуто останнього відповідного результату + Зробити фото + Відкрити галерею + Вибрати файл + Учасники групи (%s) + Додати учасників + Адмін + Видалити історію + Надайте права адміністратора + Видалити права адміністратора + Переглянути профіль контакту + Додати до контактів + Усі повідомлення будуть видалені з історії + Історію успішно видалено + %s приєднався до розмови + %s отримав права адміністратора + %s більше не є адміністратором + Контакт не знайдено + Немає адреси контакту + Не вдалося створити розмову! + Файл не вдається відкрити! + Натисніть, щоб видалити + Ви приєдналися до групи + Ви вийшли з групи + %s приєднався + %s пішов + новий пристрій для %s + нова назва: %s + %s більше не є адміном + Тимчасові повідомлення увімкнуто + Термін дії тимчасових повідомлень тепер %s + Ключ ідентифікації LIME змінено для %s + Рівень безпеки знижено через %s + Максимальна кількість учасників перевищена на %s + Медіа та документи + Спільні медіафайли + Спільні документи + Переслати повідомлення… + Перенаправлення повідомлення скасовано + Читати %s + Отримано %s + Відправлено %s + Помилка %s + Реакції %s + %1$s %2$s + Перенаправлено + запрошення на нараду: + нараду оновлено: + нараду скасовано: + голосове повідомлення + Нова нарада + Нарада + Трансляція + Інформація про трансляцію.\nДізнатися більше + Додати заголовок… + Оберіть дату початку + Оберіть час закінчення + Часовий пояс + Один раз + Додати опис + Додати учасників + Додати співрозмовника + Надіслати запрошення учасникам + Приєднатись до наради + Організатор + Нараду видалено + Нараду не знайдено! + Опис + Редагувати нараду + Скасувати нараду + Видалити нараду? + Ви хочете видалити нараду? + Видалити нараду + Нараду створено + Нараду оновлено + Нараду скасовано + Нараду скасовано + Не вдалося відредагувати нараду! + Не вдалося надіслати всі запрошення на нараду! + Не вдалося надіслати запрошення деяким учасникам наради! + Адресу наради скопійовано в буфер обміну + Приєднатися + Скасувати + Триває підключення + Не вдалося приєднатися до наради! + Вихідний дзвінок + Вхідний дзвінок + Вхідний відеодзвінок + Ви завершили дзвінок + Вхідний дзвінок для %s + Вхідний відеодзвінок для %s + Перенаправити %s на… + Поточні дзвінки + Підтвердити перенаправлення дзвінка + Додати учасників + Починає/призупиняє відтворення відео + Поділитися файлом + Зберегти файл + Зображення додано до повідомлення + Файл додано до повідомлення + Це повідомлення є відповіддю на попереднє повідомлення + Натисніть, щоб переглянути статус доставки + Починає/призупиняє відтворення голосового повідомлення + Закрити вкладення + Розмову припинено + Тимчасові повідомлення увімкнено + Закриває список учасників + Скасувати запис голосового повідомлення + Зупиняє запис голосового повідомлення + Починає запис голосового повідомлення + Надсилає повідомлення в розмові + Відкриває вибір емодзі + Відкриває вибір файлів + Натисніть, щоб редагувати назву цієї розмови + Вимкнути/увімкнути тишу в розмові + Ця розмова не захищена + Пошук угору + Пошук вниз + Почати нову розмову + Прокрутити до сьогоднішнього дня + Запланувати нараду + Редагувати нараду + Поділитися посиланням на нараду + Список учасників + Пристрій є надійним + Редагувати контакт + Завдяки технології наскрізного шифрування в &appName;, конфіденційність повідомлень, дзвінків та зустрічей гарантована. Ніхто не може розшифрувати обмін даними, навіть ми самі. + Створити подію в календарі + Неможливо створити розмову з учасником, який перебуває за межами вашого SIP-сервера, через обмеження безпеки! + Розпочати груповий дзвінок? + %s є адміністратором + Дзвінок записується + Жодного іншого дзвінка + Продовжити + Перевірте ZRTP SAS ще раз + Перенаправлення + Новий дзвінок + Список дзвінків + Номеронабирач + Повідомлення + Пауза + Запис + Завершити + Формат + В процесі + Виклик + Вхідні + Активний + Призупинено + Відновлення… + Завершено + Очікування шифрування… + Наскрізне шифрування за допомогою ZRTP + Потрібна перевірка + Наскрізне шифрування за допомогою SRTP + Дзвінок не зашифровано + Список дзвінків + %s записує + %s дзвінків + %s призупинених дзвінків + Створити конференцію + Дозвіл на аудіозапис відхилено! + Дозвіл на доступ до камери відхилено! + Перевірте пристрій + %s вийшов з розмови + Усі учасники отримають дзвінок. + пристрій для %s видалено + Тимчасові повідомлення вимкнуто + На сьогодні наради не заплановано + Не вдалося запланувати нараду! + Натисніть, щоб додати більше учасників + Ви приєднаєтеся незабаром + Співрозмовник завершив дзвінок + Для вашої безпеки нам потрібно автентифікувати пристрій вашого співрозмовника.\nБудь ласка, обміняйтеся кодами: + Будь ласка, введіть назву та виберіть хоча б одного учасника + Об\'єднати всі дзвінки в конференцію? + Конфіденційність цього дзвінка може бути скомпрометована! + Ваш код: + Код співрозмовника: + Збіги відсутні + Сповіщення безпеки + Спробуйте ще раз + Динамік + Динамік + Bluetooth (%s) + Слуховий апарат (%s) + Гарнітура + Навушники + Аудіо + Кодек: %s + Пропускна здатність: %s + Коефіцієнт втрат: %s + Jitter-буфер: %s + Відео + Роздільна здатність: %s + FPS: %s + FEC (попередня корекція помилок) + Відновлені пакети: %s + Пропускна здатність: %s + Шифрування медіа + Шифрування медіа: %s + Алгоритм шифрування: %s + Алгоритм узгодження ключів: %s + Алгоритм хешування: %s + Алгоритм автентифікації: %s + Алгоритм SAS: %s + Пристрій перевірено + Виклик перенаправляється + Перенаправлення виклику не вдалося! + Користувач зайнятий + Користувача не знайдено + Несумісні параметри медіа + Сервер не відповідає + Тимчасово недоступно + Призупинено віддалено + Поділитися запрошенням + Чекаємо на інших учасників… + Спільний доступ до екрана + Учасники + Не вдалося створити груповий дзвінок! + Не вдалося об’єднати дзвінки! + + Учасник (%s) + Учасники (%s) + Учасників (%s) + Учасників (%s) + + Видалити %s з конференції? + Ви впевнені, що хочете видалити цього учасника з конференції? + Учасника виключили з конференції + Приєднання… + Призупинено + Недійсна SIP-адреса, неможливо додати до конференції + Мозаїка + Динамік + Тільки аудіо + Віддалений груповий дзвінок + Локальний груповий дзвінок + Записи + Наразі записів немає… + Обрані + Поки що немає обраних контактів + Переглянути контакт + Копіювати SIP-адресу + Копіювати номер телефона + Видалити історію + Видалити + Дозвіл на відображення вхідних дзвінків не надано! + Пропустити + Забули пароль? + Пропустити + Нараду оновлено + Нараду скасовано! + Контакт є надійним + Контакт не надійний! + Контакт не в мережі + Відкрити меню + Повернутись + Відхилити повідомлення + Зберегти зміни + Підтвердити список нових учасників + Натисніть, щоб отримати більше інформації + Перемикання видимості пароля + Розгортає/згортає нижній аркуш + Завершення дзвінку + Відповісти на дзвінок + Почати дзвінок + Почати відеодзвінок + Вимкнути/увімкнути мікрофон + Змінює вихідний аудіопристрій + Змінити камеру (передню/задню), що використовується + Дзвінок у стані очікування + Ви записуєте цей дзвінок + Видаляє останню цифру або символ + Об\'єднує дзвінки в конференцію + Відкриває фільтр + Видалити поточний фільтр + Створює групову розмову + Розпочинає груповий дзвінок + Натисніть, щоб відобразити усі доступні опції + Видалити поле + Відобразити фільтри списку контактів + Створити контакт + Приєднатися до конференції + Видалити LDAP налаштування + Зберегти CardDAV налаштування + Видалити CardDAV налаштування + Зберегти LDAP налаштування + Відтворює запис дзвінка + Перейти до розмови + Копіювати текст у буфер обміну + Голосові повідомлення доступні + Натисніть та утримуйте для виклику голосової пошти + Для вашої безпеки нам потрібно повторно автентифікувати пристрій вашого співрозмовника.\nБудь ласка, обміняйтеся кодами повторно: + Ви справді хочете все зашифрувати? + Втрачені пакети: %s + Постквантовий ZRTP + Історію видалено + Синій + Виклик успішно перенаправлено + Сервіс недоступний або помилка мережі + Додати до контактів + Дозвіл на надсилання сповіщень не надано! + Забагато учасників для мозаїчного відображення + Надіслати повторно + Завантажити + Обраний обліковий запис наразі вимкнено + Розмови + Помилка підключення облікового запису(ів) + Пропозиції + + %s сповіщення для інших облікових записів + %s сповіщення для інших облікових записів + %s сповіщеннь для інших облікових записів + %s сповіщеннь для інших облікових записів + + Відсутнє підключення до Інтернету + Операція триває, будь ласка, зачекайте + &appName; посібник користувача + Дізнайтеся, як опанувати всі функції програми крок за кроком. + Допоможіть зробити застосунок доступним для якомога більшої кількості людей. + Надсилайте свої діагностичні журнали для полегшення вирішення помилок. + Натисніть, щоб скасувати обмін файлами або текстом, що очікує надсилання + Увімкнуто + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000000..2bb425d8af --- /dev/null +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,833 @@ + + +]> + + 安排会议 + 删除此CardDAV配置 + 将文本复制到剪贴板 + 从聊天中移除了%s + 长按可拨打语音信箱 + &appName;无法打开此文件。\n\n您想在另一个应用程序中打开它(如果可能的话),还是在您的设备上导出它? + 读取联系人:显示您的联系人并查找谁在使用 &appName;。 + 回复: + 转发消息已经取消 + 进行中 + 正在等待加密… + 丢失的数据包:%s + 消息将不再是对之前消息的回复 + 您真的要删除所有邮件吗? + 此聊天中的消息是端对端e2e加密的。只有您的对话方才能解密它们。 + 播放/暂停视频播放 + 未找到用户 + 撤消通知 + 已启用阅后即焚时消息 + 发送所有会议邀请失败! + 身份验证算法:%s + SAS算法:%s + 您确定要从会议中删除此参与者吗? + 未找到结果… + 今天没有安排会议 + 会议已取消 + 通话列表 + 为了您的安全,我们需要验证您的通信设备。\n请交换您的代码: + 哈希算法:%s + 开始视频呼叫 + 创建群组聊天 + 取消语音信息录制 + 编解码器:%s + 带宽:%s + 收藏夹 + 显示通话统计信息 + 点击查看送达状态 + Firebase 项目ID + 仅使用Wi-Fi网络 + 应用 + 认证ID + 密码 + 激活后,您必须重新启动应用程序。\n之后,所有应用程序数据都将被加密,只能通过应用程序访问。\n\n小心点,不能撤销! + 两次查询之间的延迟(毫秒) + 手机号 + 互操作模式 + MWI服务器URI(消息等待指示符) + 文件已导出到文档 + 设备ID + 昵称 + SIP域名 + 用户名 + + 下一个 + 开始 + 今天 + 昨天 + + %s 天 + + + %s 选中 + + 拒绝 + 接受 + 取消 + 继续 + 通话 + 删除 + 安装 + + + 移除 + 文件已导出为文档 + 确认 + &appName; 来电中 + &appName; 未接来电 + &appName; 服务通知 + &appName; 即时消息通知 + 响应从 %1$s 到: %2$s + 置为已读 + 回复 + 未接来电自%s + 未接群组来电自%s + %s未接来电 + 未接来电 + &appName; + 搜索新消息 + &appName; + 正在传输文件 + + %s完成上传文件 + + + %s 完成下载文件 + + 点击打开 + 欢迎 + 在&appName; + 安全 + 通讯是 端对端 安全的。 + 开源 + 一款 自由、开源应用,自 2001 + SIP地址已复制到剪贴板 + 新账户已配置 + 文件已经导出到本地相册 + 尝试将文件导出到本机相册时出错 + 媒体音量低,您可能听不到任何声音! + 配置已成功应用 + 尝试下载和应用远程配置时出错 + 尝试创建媒体播放器时出错 + 通用条款 + 无效二维码! + 充分享受&appName;我们需要您授予我们以下权限: + 我更喜欢创建一个&appName;账户 + 确认电话号码 + 您确定您的电话号码是%s吗? + 登录 + 扫描二维码 + 使用第三方SIP账户 + 第三方SIP账户 + 单点登录 + SIP地址无效! + 账户已存在 + 注册 + 确认您的电话号码 + 错误的电话号码? + 创建 + 使用您的电子邮件在以下网址创建账户: + 传输 + 我明白 + 推送通知似乎在您的设备上不可用,但在移动应用程序中创建账户时必须使用推送通知。\n\n我们邀请您在我们的网络平台上创建一个账户: + 5秒内未收到带有身份验证令牌的推送通知,请稍后重试 + 发生意外错误,请稍后重试 + 用户名或密码错误 + 登录失败:错误代码为%s + 授予权限 + OK + 以后再说 + 发布通知:当您收到消息或通话时收到通知。 + 访问摄像头:在视频通话和会议期间捕获视频。 + 忘记密码 + 还没有账户? + 已经有账户了? + 正在连接… + 什么信息&appName;收集和使用 + 在&appName;上投稿翻译 + 已连接 + 正在刷新 + 已禁用 + 错误 + 添加账户 + 帮助 + 关于 &appName; + 隐私政策 + 版本 + 检查新版本更新 + 高级 + 检查更新时出错 + 可用更新 + 新版本%s可用。您想更新吗? + 退出程序 + 故障排除 + 打印日志到logcat + 清除日志 + 分享日志 + 应用版本 + SDK版本 + 共享调试日志链接… + 调试日志已清理 + 上传调试日志失败 + 显示配置 + 您使用以下方式创建了账户: + 电话号码 + 电子邮箱 + 推送通知似乎在您的设备上不可用,但它们是在移动应用程序中恢复电话号码账户所必需的。 + 联系人 + 通话 + 聊天 + 会议 + 设置 + 安全 + 加密模块启用失败! + 加密模块已启用 + 您真的要加密所有内容吗? + 加密所有 + 防止界面被记录 + 通话 + 使用软件回声消除器 + 如果没有可用的硬件回声消除器,则防止远程端听到回声 + 校准回声消除器 + 正在进行中 + 无回声 + %s 毫秒 + 失败 + 自适应速率控制 + 启用视频 + 启用视频前向纠错FEC + 来电铃响时振动 + 自动开始录制通话 + 更改铃声 + 选择铃声 + 聊天 + 自动下载文件 + 将下载的媒体公开 + 联系人 + 添加LDAP服务器 + 编辑LDAP服务器 + 添加CardDAV通讯簿 + 编辑CardDAV通讯簿 + 昵称 + 服务器URL + 用户名 + 密码 + 认证域名 + 在其中存储新创建的联系人 + 同步成功 + 同步出错! + 当前发言人 + 服务器URL(不能为空) + 绑定DN + 密码 + 使用TLS + 搜索base(不能为空) + 搜索base(不能为空) 过滤 + 最大结果数 + 超时(秒) + 开始查询的最小字符数 + 名称属性 + SIP属性 + SIP域名 + 发生错误,LDAP服务器未保存! + 会议 + 缺省布局 + 镶嵌Mosaic + 网络 + 允许IPv6 + 用户界面 + 自动打开拨号盘 + 主题 + 深色主题 + 浅色主题 + 自动 + 主颜色 + 隧道 + 主机 + 端口 + 第二主机 + 第二主机端口 + 模式 + 禁用 + 总是 + 自动 + 高级设置 + 设备启动时启动 + 使用服务使应用程序保持活动状态 + 设备ID + 仅限字母数字字符 + &appName;安卓设置 + 日志共享服务器URL + 使用H265/AV1录制视频通话 + 媒体加密 + 强制媒体加密 + 创建端到端加密会议和群组通话 + 接受来电铃声 + 在接到来电铃声时响铃 + 允许外向的来电铃声 + 自动接听来电 + 延迟(毫秒) + 远程部署配置URL + 下载并应用 + 音频设备 + 默认输入音频设备 + 默认输出音频设备 + 音频编码 + 单声道 + 立体声 + 视频编码 + 开发人员设置 + 显示开发人员设置 + 开发人员设置已启用 + 管理账户 + 详细 + 设备 + 未找到设备… + 添加图像 + 修改图像 + 删除图像 + 账户已被禁用,您将不会收到任何呼叫或消息。 + 账户正在连接到服务器,请稍候… + 账户连接失败,请检查您的设置。 + 国际前缀 + 账户设置 + 注销账户 + 选择账户模式 + 此模式允许您享受所有&appName;功能,同时通过端对端加密与任何SIP服务保持互操作性。 + 端到端加密模式 + 删除 + 上次连接: + 退出登录并注销您的账户(该账户信息会被清除)? + 如果您想永久删除您的账户,请转到:https://sip.linphone.org + 账户设置 + 允许推送通知 + 推送通知不可用! + IM加密强制 + SIP代理服务器URL + 出站代理 + NAT策略设置 + STUN/TURN服务器URL + 启用ICE + 启用TURN + TURN用户名 + TURN密码 + AVPF + 过期(秒) + 音频/视频会议工厂URI + CCMP服务器URL + E2E加密密钥服务器URL + 捆绑模式 + 在“基本”聊天中使用CPIM + 语音邮件URI + 使用国际前缀格式化电话号码 + 格式化电话号码时,将+替换为00 + 更新密码 + 需要身份验证 + 密码 + 找不到匹配的账户! + 新呼叫 + 搜索联系人或历史通话记录 + 创建群组呼叫 + 编辑联系人 + 查看&appName;联系人 + 收藏夹 + 您好,加入我的&appName;!您可以在%s免费下载 + 设置组呼主题 + 组呼主题 + 暂时没有通话… + 聊天 + 您真的要删除所有通话记录吗? + 所有通话都将从历史记录中删除 + 所有通话都将从历史记录中删除 + 暂时没有联系人… + 目前没有SIP联系人… + 修改过滤器 + 所有联系人 + 查看所有 + 查看SIP联系人 + 新建联系人 + + + 公司 + 更改已成功保存 + 保存更改失败! + 联系人已成功创建 + 创建联系人失败! + 所有更改都将丢失 + 请填写名字、姓氏或公司名称 + 电话号码和SIP地址 + 公司: + 职位: + 信任 + 未找到设备… + 受信任设备的数量: + 其他动作 + 编辑 + 从收藏夹中删除 + 分享 + 删除 + 联系人已删除 + 目前没有联系人,也没有建议… + 提高信任度 + 删除%s? + 信任级别 + 此联系人将被永久删除。 + 选择一个电话号码或SIP地址 + 在线的 + 在%s在线 + 今天在%s在线 + 昨天在%s在线 + 请勿打扰 + 通话 + 信息 + 视频通话 + 核实 + 未命名设备 + 暂时没有聊天… + 正在删除… + %s: + + %s文件等待共享 + + 文本正在等待共享 + 标记为已读 + 静音 + 取消静音 + 通话 + 离开群组 + 配置阅后即焚消息 + 阅后即焚消息 + 禁用 + 1分钟 + 1小时 + 1天 + 3天 + 1周 + 新建群组聊天 + 新聊天 + 请输入聊天的名称 + 搜索联系人 + 创建群组聊天 + 离开了 + 未找到媒体… + 说点什么… + + %s正在输入… + + 搜索 + 聊天信息 + 阅后即焚消息 + 媒体 + 文档 + 未找到文档… + 端到端加密聊天 + 保证机密性 + 此聊天未加密! + 为了您的安全,此聊天已被禁用。 + 已达到附件的最大数量! + 设置聊天主题 + 编辑聊天主题 + 主题为必填项 + 聊天主题 + 打开还是导出文件? + 打开文件 + 导出文件 + 以纯文本形式打开? + 找不到打开此类文件的应用程序。\n\n您想尝试以纯文本形式打开它吗? + 以纯文本形式打开 + 无法播放录音! + 消息已被删除 + 创建聊天失败! + 由于安全限制,无法与不在同一域中的参与者创建聊天! + 添加参与者 + 未找到所选媒体 + 聊天主题已更改 + 已启用阅后即焚消息 + 已禁用阅后即焚消息 + %s的新设备 + 已达到最大持续时间 + 聊天已成功删除 + 您已离开该组 + 找不到打开此类文件的应用程序 + 未找到聊天 + 未找到匹配结果 + 已达到上次匹配结果 + 拍照 + 打开相册 + 选择文件 + 无法打开文件! + 组成员(%s) + 添加参与者 + 管理员 + 删除历史 + 从组中删除 + 授予管理员权限 + 删除管理员权限 + 查看联系人资料 + 添加到联系人 + 所有消息都将从历史记录中删除 + 历史记录已成功删除 + %s加入了聊天 + %s现在是管理员 + %s不再是管理员 + 未找到联系人 + 开始群组通话? + 所有参与者都将接到通话。 + 您已加入该群 + 您已离开该组 + %s已加入 + %s已离开 + %s的设备已删除 + 新主题:%s + %s是管理员 + %s不再是管理员 + 已禁用阅后即焚时消息 + %s的LIME标识密钥已更改 + 检测到%s的中间人攻击 + 由于%s,安全级别降低 + 超过最大参与者数%s + 媒体和文档 + 分享媒体 + 分享文档 + 消息已转发 + 转发消息到… + 错误 %s + 已发送 %s + 已收到 %s + 读取 %s + 响应 %s + %1$s %2$s + 点击以删除 + 已转发 + 会议邀请: + 会议更新: + 会议取消: + 语音信息 + 新建会议 + 会议 + 广播 + 关于广播的信息。\n 了解更多 + 添加标题… + 选择开始日期 + 选择开始时间 + 选择结束时间 + 时区 + 一次 + 添加描述 + 添加参与者 + 添加发言人 + 向参与者发送邀请 + 立即加入会议 + 主办单位 + 创建日历事件 + 会议已被删除 + 视频来电 %s + 来电%s + 描述 + 编辑会议 + 取消会议 + 删除会议? + 您想删除会议吗? + 删除会议 + 会议已创建 + 会议已更新 + 已取消会议 + 安排会议失败! + 编辑会议失败! + 请填写标题并选择至少一名参与者 + 未能向会议的某些参与者发送邀请! + 会议地址已复制到剪贴板 + 加入 + 取消 + 连接正在进行中 + 您很快就会加入 + 加入会议失败! + 呼出 + 呼入 + 呼入视频 + 您已结束通话 + 当前通话 + 没有其他通话 + 确认呼叫转移 + 呼叫转移%s到… + 您即将将呼叫%1$s转接到%2$s。 + 呼叫转移 + 新建通话 + 拨号 + 消息 + 暂停 + 恢复 + 录音 + 挂断 + 布局 + 振铃中 + 来电 + 暂停的 + 活动的 + 被远程方暂停 + 正在恢复… + 结束了 + ZRTP端到端加密 + 再次验证ZRTP SAS + 需要验证 + SRTP点对点加密 + 通话未加密 + 通话列表 + 通话正在录音 + %s正在录制 + %s通话 + %s暂停通话 + 创建会议 + 录音权限被拒绝! + 相机权限被拒绝! + 验证设备 + 为了您的安全,我们需要重新验证您的通信设备。\n请重新交换您的代码: + 您的验证码: + 对话人验证码: + 没有匹配项 + 安全警报 + 再试一次 + 此通话的保密性可能会受到损害! + 耳机 + 发言人 + Bluetooth (%s) + 助听器(%s) + 头戴式耳机 + 耳机 + 音频 + 带宽:%s + 损失率:%s + 抖动缓冲区:%s + 视频 + 分辨率:%s + 帧率FPS:%s + 前向错误更正FEC + 已修复的数据包:%s + 媒体加密 + 媒体加密:%s + 后量子Post Quantum ZRTP + 密码算法:%s + 密钥协商算法:%s + 查看联系人 + 历史记录已被删除 + 设备已验证 + 呼叫正在转接 + 呼叫已成功转移 + 呼叫转移失败! + 用户正忙 + 不兼容的媒体参数 + 服务不可用或网络错误 + 服务器超时 + 暂时不可用 + 分享邀请 + 正在等待其他参与者… + 屏幕共享 + 创建群组呼叫失败! + 合并呼叫失败! + + 参与者(%s) + + 是否从会议中删除%s? + 正在加入… + 参与者被逐出会议 + 暂停 + 正在共享其屏幕 + SIP地址无效,无法添加到会议中 + 镶嵌Mosaic + 发言人 + 仅限音频 + 镶嵌布局的参与者太多 + 远程组呼 + 本地组呼叫 + 录音 + 暂时没有录音… + 还没有收藏夹联系人 + 添加到联系人 + 复制SIP地址 + 复制电话号码 + 删除历史 + 删除 + 邀请 + 重新发送 + 忘记密码? + 回复 + 转发 + 复制 + 下载 + 分享 + 黄色 + 绿色 + 蓝色 + 红色 + 粉红色 + 紫色 + 橙色 + 选定的参与者将显示在此处 + 账户连接错误 + + %s通知其他账户 + + 所选账户当前已禁用 + 您未连接到internet + 操作正在进行中,请稍候 + 聊天 + 联系人 + 收藏夹 + 建议 + 未授予发布通知权限! + + %s新语音信息 + + 跳过 + 跳过 + 会议已更新 + 会议已取消! + 联系人值得信赖 + 联系人不可信! + 联系人在线 + 联系人不在线 + 打开抽屉菜单 + 返回 + 保存更改 + 显示菜单 + 确认新参与者名单 + 点击以获取更多信息 + 点击以删除参与者 + 展开/缩回底板 + 终止通话 + 接听通话 + 接听视频通话 + 开始呼叫 + 启用/禁用发送相机馈送 + 将麦克风静音/取消静音 + 更改输出音频设备 + 更换正在使用的摄像头(前/后) + 通话处于暂停状态 + 您正在录制此通话 + 删除最后一个数字或字符 + 将通话合并到会议中 + 打开过滤区域 + 关闭过滤区域 + 清除当前过滤器 + 开始群组呼叫 + 显示数字拨号盘 + 单击以显示所有可用选项 + 参与者已静音 + 参与者正在发言 + 添加参与者 + 播放/暂停音频播放 + 分享文件 + 保存文件 + 图像附在消息中 + 视频附在消息中 + 文件附在消息中 + 此消息是对之前消息的回复 + 此消息是从另一个聊天转发的 + 播放/暂停语音信息播放 + 从附件中删除此文件 + 关闭附件 + 已压制聊天使不说话 + 启用了阅后即焚消息 + 滚动到第一条未读消息或底部 + 关闭参与者列表 + 打开表情符号选取器 + 打开文件选择器 + 单击以编辑此聊天的主题 + 压制此聊天使其不说话开/关 + 正在删除聊天 + 停止语音信息录制 + 开始录制语音信息 + 在聊天中发送消息 + 此聊天不安全 + 向上搜索 + 向下搜索 + 开始新的聊天 + 滚动到今天 + 安排会议 + 编辑会议 + 分享会议地址 + 参与者列表 + 设备受信任 + 编辑联系人 + 删除字段 + 显示联系人列表过滤器 + 创建联系人 + 加入会议 + 保存CardDAV配置 + 删除此LDAP配置 + 保存LDAP配置 + 播放通话录音 + 转到聊天 + 语音信息可用 + SIP地址 + 通过在崩溃后向Crashlytics发送日志来帮助开发人员解决问题 + 这个账户在线,大家可以和您通话。 + 选择您的国家以允许&appName;匹配您的联系人。 + 不再显示此对话框 + 使用两台服务器 + 开发人员设置已启用 + &appName; 通话中 + 文件共享服务器URL + 此模式保证您的数据机密性。我们的端到端加密技术为您的通信提供最高级别的安全保障。 + 不保存更改? + 继续,即表示您接受我们的%1$s和%2$s。 + 某些功能需要&appName;账户,如群消息、视频会议…\n\n当您使用第三方SIP账户注册时,这些功能将被隐藏。\n\n要在商业项目中启用它,请联系我们。 + 此服务将一直运行,以保持应用程序的活动状态,并允许您接收来电和消息。 + 没有配置账户 + 在取消消息通知时将聊天标记为已读 + 自动应答呼叫前的延迟 + 您真的要删除与该人的历史记录吗? + 通用条款和隐私政策 + 一款 安全开源法国 通讯应用app。 + 管理个人资料 + 将使用专有文件格式 + 版本是最新的 + 连接失败,因为账户缺少身份验证或身份验证无效。\n%s\n\n您可以再次提供密码,或在设置中检查您的账户配置。 + 警告:一旦启用,就不能禁用! + CardDAV账户已删除 + 隐私政策 + SIP地址不包含用户名! + 请至少填写昵称和服务器URL + 会议工厂URI + 目前没有建议,也没有联系人… + 职位 + 添加到收藏夹 + 我们已给您的电话号码%1$s发送了验证码。\n\n请在下面输入验证码: + 选择如何恢复您的账户。 + 未能将参与者添加到聊天中 + 录制音频:这样您的对话方就可以听到您的声音并录制语音信息。 + 对方已结束通话 + 将所有通话合并到会议中? + 仅支持Wi-Fi模式 + 数字已复制到剪贴板 + 阅后即焚消息生命周期时间已更改 + 送达状态 + 切换密码可见性 + 得益于&appName;中的端到端加密技术保证信息、通话和会议的保密性。没有人能解密交换的数据,即使是我们自己。 + 您将要呼叫%1$s的设备%2$s。\n您想呼叫吗? + 删除聊天 + 单击以添加更多参与者 + 找不到会议! + 参与者 + 检查您的所有联系设备,以确保您的通信安全,不受任何更改。\n当所有内容都得到验证时,您将达到最大信任级别。 + 新消息一旦被所有人阅读,就会自动删除。\n选择持续时间: + 没有匹配结果… + 没有地址要添加到联系人 + 阅后即焚消息生命周期现在是%s + 未授予显示来电权限! + 逐步学习如何掌握所有应用程序功能。 + &appName; 用户指南 + 帮助尽可能多的人访问应用程序。 + 传输您的诊断日志以促进错误解决。 + 单击可取消待共享的文件或文本 + 已启用 + ZRTP点对点加密 + 连接已断开 + 订阅在线状态信息 + 该账户处于离线状态,可能是由于您当前未连接到互联网。 + diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 8830b12bb5..9c5f0284fc 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -104,7 +104,6 @@ 300dp 300dp 425dp - 340dp 15dp 30dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2794485d63..af4aaff6d8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -548,10 +548,6 @@ %s is composing… %s are composing… - - %s is recording a voice message… - %s are recording a voice message… - Add participants Replying to: Search @@ -570,8 +566,6 @@ Messages exchanged in this conversation can be intercepted and read by other people than your correspondent, confidentiality is not guaranteed! This conversation is not encrypted! For your safety, this conversation was disabled. - Mandatory encryption - You enabled mandatory encryption. Unencrypted conversations are disabled for your safety. You can re-create this conversation or disable mandatory encryption in your account parameters. Maximum number of attachments reached! Set conversation subject Edit conversation subject diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index d0d04b9ee0..3cbb8fac2a 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -1,5 +1,15 @@ + + + + + + + + + + \ No newline at end of file From 2a9ef440b7d9d15559d9b175663d80291bf62648 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 5 Dec 2025 11:20:09 +0100 Subject: [PATCH 368/593] Code small improvements --- .../notifications/NotificationsManager.kt | 2 +- .../telecom/TelecomCallControlCallback.kt | 7 -- .../ConferenceParticipantsListAdapter.kt | 2 +- .../ui/call/fragment/TransferCallFragment.kt | 5 +- .../ui/fileviewer/viewmodel/FileViewModel.kt | 3 +- ...ationsContactsAndSuggestionsListAdapter.kt | 4 +- .../chat/adapter/ConversationEventAdapter.kt | 6 +- .../ConversationParticipantsAdapter.kt | 2 +- .../chat/adapter/ConversationsFilesAdapter.kt | 4 +- .../chat/adapter/MessageBottomSheetAdapter.kt | 2 +- .../adapter/ContactHistoryListAdapter.kt | 2 +- .../adapter/MeetingParticipantsAdapter.kt | 2 +- .../meetings/adapter/MeetingsListAdapter.kt | 2 +- .../linphone/ui/welcome/WelcomeActivity.kt | 2 +- .../java/org/linphone/utils/DialogUtils.kt | 17 --- .../linphone/utils/PatternClickableSpan.kt | 2 +- .../java/org/linphone/utils/ShortcutUtils.kt | 17 +-- .../main/res/layout/dialog_delete_meeting.xml | 106 ------------------ 18 files changed, 28 insertions(+), 159 deletions(-) delete mode 100644 app/src/main/res/layout/dialog_delete_meeting.xml diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 79476bcec8..f3291e76b9 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -1209,7 +1209,7 @@ class NotificationsManager .setContentIntent(pendingIntent) .build() - accountsErrorNotificationsMap.put(identity, notificationId) + accountsErrorNotificationsMap[identity] = notificationId Log.i("$TAG Showing account registration error notification with ID [$notificationId] for [$identity]") notify(notificationId, notification, ACCOUNT_ERROR_TAG) } diff --git a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt index e9953930d4..58e06d5121 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomCallControlCallback.kt @@ -46,15 +46,8 @@ class TelecomCallControlCallback( ) { companion object { private const val TAG = "[Telecom Call Control Callback]" - - private const val DELAY_BEFORE_RELOADING_SOUND_DEVICES_MS = 100L } - private var availableEndpoints: List = arrayListOf() - private var currentEndpoint = CallEndpointCompat.TYPE_UNKNOWN - private var endpointUpdateRequestFromLinphone: Boolean = false - private var latestLinphoneRequestedEndpoint: CallEndpointCompat? = null - private var mutedByTelecomManager = false private val callListener = object : CallListenerStub() { diff --git a/app/src/main/java/org/linphone/ui/call/adapter/ConferenceParticipantsListAdapter.kt b/app/src/main/java/org/linphone/ui/call/adapter/ConferenceParticipantsListAdapter.kt index f56514bbaf..8147629257 100644 --- a/app/src/main/java/org/linphone/ui/call/adapter/ConferenceParticipantsListAdapter.kt +++ b/app/src/main/java/org/linphone/ui/call/adapter/ConferenceParticipantsListAdapter.kt @@ -49,7 +49,7 @@ class ConferenceParticipantsListAdapter : (holder as ViewHolder).bind(getItem(position)) } - inner class ViewHolder( + class ViewHolder( val binding: CallConferenceParticipantListCellBinding ) : RecyclerView.ViewHolder(binding.root) { @UiThread diff --git a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt index 8f42bc2818..5fc4193fa3 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt @@ -332,9 +332,8 @@ class TransferCallFragment : GenericCallFragment() { model.confirmEvent.observe(viewLifecycleOwner) { it.consume { coreContext.postOnCoreThread { - val address = toAddress - Log.i("$TAG Transferring (blind) call to [${address.asStringUriOnly()}]") - callViewModel.blindTransferCallTo(address) + Log.i("$TAG Transferring (blind) call to [${toAddress.asStringUriOnly()}]") + callViewModel.blindTransferCallTo(toAddress) } dialog.dismiss() diff --git a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt index b35c042e60..d44f965d1f 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt @@ -130,8 +130,7 @@ class FileViewModel val extension = FileUtils.getExtensionFromFileName(file) val mime = FileUtils.getMimeTypeFromExtension(extension) mimeType.postValue(mime) - val mimeType = FileUtils.getMimeType(mime) - when (mimeType) { + when (val mimeType = FileUtils.getMimeType(mime)) { FileUtils.MimeType.Pdf -> { Log.d("$TAG File [$file] seems to be a PDF") loadPdf() diff --git a/app/src/main/java/org/linphone/ui/main/adapter/ConversationsContactsAndSuggestionsListAdapter.kt b/app/src/main/java/org/linphone/ui/main/adapter/ConversationsContactsAndSuggestionsListAdapter.kt index 356fb10bbb..e8c79d2b00 100644 --- a/app/src/main/java/org/linphone/ui/main/adapter/ConversationsContactsAndSuggestionsListAdapter.kt +++ b/app/src/main/java/org/linphone/ui/main/adapter/ConversationsContactsAndSuggestionsListAdapter.kt @@ -161,7 +161,7 @@ class ConversationsContactsAndSuggestionsListAdapter : } } - inner class ConversationViewHolder( + class ConversationViewHolder( val binding: GenericAddressPickerConversationListCellBinding ) : RecyclerView.ViewHolder(binding.root) { @UiThread @@ -198,7 +198,7 @@ class ConversationsContactsAndSuggestionsListAdapter : } } - inner class SuggestionViewHolder( + class SuggestionViewHolder( val binding: GenericAddressPickerSuggestionListCellBinding ) : RecyclerView.ViewHolder(binding.root) { @UiThread diff --git a/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationEventAdapter.kt b/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationEventAdapter.kt index c02f7097b7..c3d345d1bb 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationEventAdapter.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationEventAdapter.kt @@ -202,7 +202,7 @@ class ConversationEventAdapter : } } - inner class IncomingBubbleViewHolder( + class IncomingBubbleViewHolder( val binding: ChatBubbleIncomingBinding ) : RecyclerView.ViewHolder(binding.root) { fun bind(message: MessageModel) { @@ -217,7 +217,7 @@ class ConversationEventAdapter : } } - inner class OutgoingBubbleViewHolder( + class OutgoingBubbleViewHolder( val binding: ChatBubbleOutgoingBinding ) : RecyclerView.ViewHolder(binding.root) { fun bind(message: MessageModel) { @@ -232,7 +232,7 @@ class ConversationEventAdapter : } } - inner class EventViewHolder( + class EventViewHolder( val binding: ChatConversationEventBinding ) : RecyclerView.ViewHolder(binding.root) { fun bind(event: EventModel) { diff --git a/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationParticipantsAdapter.kt b/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationParticipantsAdapter.kt index f72af30acd..95cca4bd31 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationParticipantsAdapter.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationParticipantsAdapter.kt @@ -50,7 +50,7 @@ class ConversationParticipantsAdapter : ListAdapter = ArrayList() - inner class SpannablePatternItem( + class SpannablePatternItem( var pattern: Pattern, var listener: SpannableClickedListener ) diff --git a/app/src/main/java/org/linphone/utils/ShortcutUtils.kt b/app/src/main/java/org/linphone/utils/ShortcutUtils.kt index 700d082c29..6a5cc88213 100644 --- a/app/src/main/java/org/linphone/utils/ShortcutUtils.kt +++ b/app/src/main/java/org/linphone/utils/ShortcutUtils.kt @@ -165,14 +165,15 @@ class ShortcutUtils { .setIsConversation() .setLongLived(Version.sdkAboveOrEqual(Version.API30_ANDROID_11)) .setLocusId(LocusIdCompat(id)) - // See https://developer.android.com/training/sharing/direct-share-targets#track-shortcut-usage-comms-apps - if (isGroup) { - builder.addCapabilityBinding("actions.intent.SEND_MESSAGE", "message.recipient.@type", listOf("Audience")) - builder.addCapabilityBinding("actions.intent.RECEIVE_MESSAGE", "message.sender.@type", listOf("Audience")) - } else { - builder.addCapabilityBinding("actions.intent.SEND_MESSAGE") - builder.addCapabilityBinding("actions.intent.RECEIVE_MESSAGE") - } + + // See https://developer.android.com/training/sharing/direct-share-targets#track-shortcut-usage-comms-apps + if (isGroup) { + builder.addCapabilityBinding("actions.intent.SEND_MESSAGE", "message.recipient.@type", listOf("Audience")) + builder.addCapabilityBinding("actions.intent.RECEIVE_MESSAGE", "message.sender.@type", listOf("Audience")) + } else { + builder.addCapabilityBinding("actions.intent.SEND_MESSAGE") + builder.addCapabilityBinding("actions.intent.RECEIVE_MESSAGE") + } return builder.build() } catch (e: NumberFormatException) { diff --git a/app/src/main/res/layout/dialog_delete_meeting.xml b/app/src/main/res/layout/dialog_delete_meeting.xml deleted file mode 100644 index c8d20e218d..0000000000 --- a/app/src/main/res/layout/dialog_delete_meeting.xml +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From e1abcc6dcaac13eb646204776029bcb3e3213e10 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 8 Dec 2025 15:04:43 +0100 Subject: [PATCH 369/593] Fixed ContactLoader not notifying app of changes when editing a native friend through another app --- .../java/org/linphone/contacts/ContactLoader.kt | 14 ++++++++++++-- .../ui/main/contacts/viewmodel/ContactViewModel.kt | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactLoader.kt b/app/src/main/java/org/linphone/contacts/ContactLoader.kt index 48fb2d7446..75682a8a99 100644 --- a/app/src/main/java/org/linphone/contacts/ContactLoader.kt +++ b/app/src/main/java/org/linphone/contacts/ContactLoader.kt @@ -99,8 +99,10 @@ class ContactLoader : LoaderManager.LoaderCallbacks { ContactsContract.Data.CONTACT_ID + " ASC" ) - // Update at most once every X (see variable value for actual duration) - loader.setUpdateThrottle(MIN_INTERVAL_TO_WAIT_BEFORE_REFRESH) + // WARNING: this doesn't prevent to be called again in onLoadFinished, + // it will only have for effect that the notified cursor will be the same as before + // instead of a new one with updated content! + // loader.setUpdateThrottle(MIN_INTERVAL_TO_WAIT_BEFORE_REFRESH) return loader } @@ -110,8 +112,16 @@ class ContactLoader : LoaderManager.LoaderCallbacks { if (cursor == null) { Log.e("$TAG Cursor is null!") return + } else if (cursor.isClosed) { + Log.e("$TAG Cursor is closed!") + return } + Log.i("$TAG Load finished, found ${cursor.count} entries in cursor") + if (cursor.isAfterLast) { + Log.w("$TAG Cursor position is after last, it was probably already used, nothing to do") + return + } coreContext.postOnCoreThread { val core = coreContext.core diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt index 96f8b99545..31a06fe13c 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt @@ -181,7 +181,7 @@ class ContactViewModel if (!::friend.isInitialized) return val found = coreContext.contactsManager.findContactById(refKey) - if (found != null && found != friend) { + if (found != null) { Log.i( "$TAG Found contact [${found.name}] matching ref key [$refKey] after contacts have been loaded/updated" ) From 618be9ee7cf9c9cdd34a8bbb8f39d14c46feeda5 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 8 Dec 2025 15:25:23 +0100 Subject: [PATCH 370/593] Fixed contact not updated when company or job title was removed from native contact --- .../ui/main/contacts/viewmodel/ContactViewModel.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt index 31a06fe13c..9da8cd5bb1 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt @@ -321,13 +321,10 @@ class ContactViewModel contact.postValue(ContactAvatarModel(friend)) val organization = friend.organization - if (!organization.isNullOrEmpty()) { - company.postValue(organization!!) - } + company.postValue(organization.orEmpty()) + val jobTitle = friend.jobTitle - if (!jobTitle.isNullOrEmpty()) { - title.postValue(jobTitle!!) - } + title.postValue(jobTitle.orEmpty()) val addressesAndNumbers = friend.getListOfSipAddressesAndPhoneNumbers(listener) sipAddressesAndPhoneNumbers.postValue(addressesAndNumbers) From d2b12159af237ccad51eacfcaff67716577b77d4 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 11 Dec 2025 10:25:46 +0100 Subject: [PATCH 371/593] Allow linphone-config URIs in QR codes scanned inside Linphone --- .../ui/assistant/viewmodel/QrCodeViewModel.kt | 39 ++++++++++--------- .../java/org/linphone/ui/main/MainActivity.kt | 10 ++--- .../java/org/linphone/utils/LinphoneUtils.kt | 21 ++++++++++ 3 files changed, 46 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt index e0de49fbc9..643b488483 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt @@ -19,7 +19,6 @@ */ package org.linphone.ui.assistant.viewmodel -import android.util.Patterns import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData @@ -32,6 +31,7 @@ import org.linphone.ui.GenericViewModel import org.linphone.utils.Event import org.linphone.R import org.linphone.core.GlobalState +import org.linphone.utils.LinphoneUtils class QrCodeViewModel @UiThread @@ -76,26 +76,27 @@ class QrCodeViewModel if (result == null) { showRedToast(R.string.assistant_qr_code_invalid_toast, R.drawable.warning_circle) } else { - val isValidUrl = Patterns.WEB_URL.matcher(result).matches() - if (!isValidUrl) { - Log.e("$TAG The content of the QR Code doesn't seem to be a valid web URL") + val url = LinphoneUtils.getRemoteProvisioningUrlFromUri(result) + if (url == null) { + Log.e("$TAG The content of the QR Code [$result] doesn't seem to be a valid web URL") showRedToast(R.string.assistant_qr_code_invalid_toast, R.drawable.warning_circle) - } else { - Log.i( - "$TAG QR code URL set, restarting the Core outside of iterate() loop to apply configuration changes" - ) - core.nativePreviewWindowId = null - core.isVideoPreviewEnabled = false - core.isQrcodeVideoPreviewEnabled = false - core.provisioningUri = result + return + } - coreContext.postOnCoreThread { core -> - Log.i("$TAG Stopping Core") - coreContext.core.stop() - Log.i("$TAG Core has been stopped, restarting it") - coreContext.core.start() - Log.i("$TAG Core has been restarted") - } + Log.i( + "$TAG Setting QR code URL [$url], restarting the Core outside of iterate() loop to apply configuration changes" + ) + core.nativePreviewWindowId = null + core.isVideoPreviewEnabled = false + core.isQrcodeVideoPreviewEnabled = false + core.provisioningUri = url + + coreContext.postOnCoreThread { core -> + Log.i("$TAG Stopping Core") + core.stop() + Log.i("$TAG Core has been stopped, restarting it") + core.start() + Log.i("$TAG Core has been restarted") } } } diff --git a/app/src/main/java/org/linphone/ui/main/MainActivity.kt b/app/src/main/java/org/linphone/ui/main/MainActivity.kt index 56c69c961f..f50b4cd9df 100644 --- a/app/src/main/java/org/linphone/ui/main/MainActivity.kt +++ b/app/src/main/java/org/linphone/ui/main/MainActivity.kt @@ -790,11 +790,11 @@ class MainActivity : GenericActivity() { } private fun handleConfigIntent(uri: String) { - val remoteConfigUri = uri.substring("linphone-config:".length) - val url = when { - remoteConfigUri.startsWith("http://") || remoteConfigUri.startsWith("https://") -> remoteConfigUri - remoteConfigUri.startsWith("file://") -> remoteConfigUri - else -> "https://$remoteConfigUri" + Log.i("$TAG Trying to parse config intent [$uri] as remote provisioning URL") + val url = LinphoneUtils.getRemoteProvisioningUrlFromUri(uri) + if (url == null) { + Log.e("$TAG Couldn't parse URI [$uri] into a valid remote provisioning URL, aborting") + return } coreContext.postOnCoreThread { core -> diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 511df23d78..5ddcede1e3 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -23,6 +23,7 @@ import android.graphics.Typeface import android.text.Spannable import android.text.SpannableStringBuilder import android.text.style.StyleSpan +import android.util.Patterns import androidx.annotation.AnyThread import androidx.annotation.DrawableRes import androidx.annotation.IntegerRes @@ -65,6 +66,26 @@ class LinphoneUtils { const val RECORDING_MKV_FILE_EXTENSION = ".mkv" const val RECORDING_SMFF_FILE_EXTENSION = ".smff" + @AnyThread + fun getRemoteProvisioningUrlFromUri(uri: String): String? { + val linphoneScheme = "linphone-config:" + return if (uri.startsWith(linphoneScheme)) { + val remoteConfigUri = uri.substring(linphoneScheme.length) + val url = when { + remoteConfigUri.startsWith("http://") || remoteConfigUri.startsWith("https://") -> remoteConfigUri + remoteConfigUri.startsWith("file://") -> remoteConfigUri + else -> "https://$remoteConfigUri" + } + url + } else { + val isValidUrl = Patterns.WEB_URL.matcher(uri).matches() + if (!isValidUrl) { + return null + } + uri + } + } + @WorkerThread fun getDefaultAccount(): Account? { return coreContext.core.defaultAccount ?: coreContext.core.accountList.firstOrNull() From dce7095f74ca743adf08d4d2b015b540df987f55 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 11 Dec 2025 11:09:25 +0100 Subject: [PATCH 372/593] Added workaround for linphone-config:// URIs --- app/src/main/java/org/linphone/utils/LinphoneUtils.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 5ddcede1e3..a125feb4a9 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -74,6 +74,7 @@ class LinphoneUtils { val url = when { remoteConfigUri.startsWith("http://") || remoteConfigUri.startsWith("https://") -> remoteConfigUri remoteConfigUri.startsWith("file://") -> remoteConfigUri + remoteConfigUri.startsWith("//") -> "https:$remoteConfigUri" else -> "https://$remoteConfigUri" } url From 3711fd749e2cc30bd62632164cf4401dd214abed Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 12 Dec 2025 10:15:00 +0100 Subject: [PATCH 373/593] Bumped AGP to 8.13.2 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4130128bf9..c3613201e7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.13.1" +agp = "8.13.2" kotlin = "2.2.21" gmsGoogleServices = "4.4.4" firebaseCrashlytics = "3.0.6" From ff98c15840f44dd88c57e6006fc4003880cac7d8 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 15 Dec 2025 09:40:15 +0100 Subject: [PATCH 374/593] Fixed crash due to empty routes & when setting an empty one in account params --- .../ui/main/settings/viewmodel/AccountSettingsViewModel.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt index 1aaafd2114..89ebbbf5cf 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt @@ -27,6 +27,7 @@ import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.core.AVPFMode import org.linphone.core.Account +import org.linphone.core.Address import org.linphone.core.AuthInfo import org.linphone.core.Factory import org.linphone.core.NatPolicy @@ -155,7 +156,9 @@ class AccountSettingsViewModel selectedTransport.postValue(transportType) sipProxyServer.postValue(params.serverAddress?.asStringUriOnly()) - outboundProxyServer.postValue(params.routesAddresses.first().asStringUriOnly()) + if (params.routesAddresses.isNotEmpty()) { + outboundProxyServer.postValue(params.routesAddresses.first().asStringUriOnly()) + } natPolicy = params.natPolicy ?: core.createNatPolicy() stunServer.postValue(natPolicy.stunServer) @@ -242,7 +245,7 @@ class AccountSettingsViewModel Log.e("$TAG Failed to parse outbound proxy server!") } } else { - newParams.setRoutesAddresses(null) + newParams.setRoutesAddresses(arrayOf
()) } if (::natPolicy.isInitialized) { From e8c67fdd6f82a2adae8a58922a4a9a2d6db8da0b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 15 Dec 2025 11:20:31 +0100 Subject: [PATCH 375/593] Don't use connected bluetooth audio device (if any) for recording a voice message --- app/src/main/java/org/linphone/utils/AudioUtils.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/linphone/utils/AudioUtils.kt b/app/src/main/java/org/linphone/utils/AudioUtils.kt index 83762908c2..6ffa2e4d02 100644 --- a/app/src/main/java/org/linphone/utils/AudioUtils.kt +++ b/app/src/main/java/org/linphone/utils/AudioUtils.kt @@ -213,7 +213,6 @@ class AudioUtils { // In case no headset/hearing aid/bluetooth is connected, use microphone sound card // If none are available, default one will be used var headsetCard: AudioDevice? = null - var bluetoothCard: AudioDevice? = null var microphoneCard: AudioDevice? = null for (device in coreContext.core.audioDevices) { if (device.hasCapability(AudioDevice.Capabilities.CapabilityRecord)) { @@ -221,9 +220,6 @@ class AudioUtils { AudioDevice.Type.Headphones, AudioDevice.Type.Headset -> { headsetCard = device } - AudioDevice.Type.Bluetooth, AudioDevice.Type.HearingAid -> { - bluetoothCard = device - } AudioDevice.Type.Microphone -> { microphoneCard = device } @@ -232,9 +228,9 @@ class AudioUtils { } } Log.i( - "$TAG Found headset/headphones sound card [$headsetCard], bluetooth/hearingAid sound card [$bluetoothCard] and microphone card [$microphoneCard]" + "$TAG Found headset/headphones sound card [$headsetCard] and microphone card [$microphoneCard]" ) - return headsetCard ?: bluetoothCard ?: microphoneCard + return headsetCard ?: microphoneCard } @AnyThread From be47deeb40bda74262ad9c747038ca904b9211d6 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 15 Dec 2025 14:15:13 +0100 Subject: [PATCH 376/593] Fixed self avatar not displayed in call views --- .../linphone/ui/call/viewmodel/CurrentCallViewModel.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 07353b2ab7..7ebce0ddc8 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -1193,7 +1193,8 @@ class CurrentCallViewModel val model = if (conferenceInfo != null) { coreContext.contactsManager.getContactAvatarModelForConferenceInfo(conferenceInfo) } else { - // Do not use contact avatar model from ContactsManager + // Do not use contact avatar model from ContactsManager to be able to show + // ZRTP verification status with the device that will answer the call val friend = coreContext.contactsManager.findContactByAddress(address) if (friend != null) { ContactAvatarModel(friend, address) @@ -1201,6 +1202,12 @@ class CurrentCallViewModel val fakeFriend = coreContext.core.createFriend() fakeFriend.name = LinphoneUtils.getDisplayName(address) fakeFriend.address = address + val localAccount = coreContext.core.accountList.find { + it.params.identityAddress?.weakEqual(address) == true + } + if (localAccount != null) { + fakeFriend.photo = localAccount.params.pictureUri + } ContactAvatarModel(fakeFriend, address) } } From 00b8e59ade84af5546f15e705d32ef05b52c0586 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 16 Dec 2025 16:23:11 +0100 Subject: [PATCH 377/593] Updated CHANGELOG with 6.0.21 release info --- CHANGELOG.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40c250c559..9183cb4822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,10 +49,25 @@ Group changes to describe their impact on the project, as follows: - Permission fragment will only show missing ones - Added more info into StartupListener logs - Updated password forgotten procedure, will use online account manager platform -- Added back "large heap" to AndroidManifest + +## [6.0.21] - 2025-12-16 + +### Added +- Allow linphone-config: scheme URIs in in-app QR code scanner + +### Changed +- Workaround for audio focus & audio manager mode on devices that do not support TelecomManager APIs +- Set front camera as default after using back camera when scanning a QR code +- Added back largeHeap flag in AndroidManifest.xml ### Fixed -- No audio focus & wrong audio manager mode when TelecomManager isn't supported by device +- Fixed call recording indicator not showing local record in progress in case UPDATE isn't answered +- Fixed native addressbook reload when a contact is updated in the OS default app +- Fixed issue with linphone-config scheme URIs if scheme is followed by "//" +- Fixed Job & Company contact field not updated if field content was removed +- Fixed local avatar not displayed when calling ourselves +- Prevent crashes due to some ActivityNotFound exceptions +- Prevent crash due to empty clipboard on some devices ## [6.0.20] - 2025-11-21 From 965b159139112618cccfe7dfe4ef942d223fb429 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 15 Dec 2025 13:07:14 +0100 Subject: [PATCH 378/593] Using newly added MWI API to only show the notification bar for the concerned account + call voicemail when clicking on it --- .../ui/main/viewmodel/MainViewModel.kt | 82 +++++++++++++++---- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt index afb6275427..1891ba0cf9 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt @@ -126,6 +126,8 @@ class MainViewModel private var nonDefaultAccountNotificationsCount = 0 + private var mwiNewMessages = false + private val coreListener = object : CoreListenerStub() { @WorkerThread override fun onGlobalStateChanged(core: Core, state: GlobalState?, message: String) { @@ -302,6 +304,14 @@ class MainViewModel ) addAlert(DEFAULT_ACCOUNT_DISABLED, label) } + + val mwi = account.latestReceivedMessageWaitingIndication + if (mwi != null) { + parseMwiEvent(mwi) + } else { + removeAlert(MWI_MESSAGES_WAITING) + mwiNewMessages = false + } } computeNonDefaultAccountNotificationsCount() @@ -334,23 +344,10 @@ class MainViewModel event: org.linphone.core.Event, mwi: MessageWaitingIndication ) { - if (mwi.hasMessageWaiting()) { - val summaries = mwi.summaries - Log.i( - "$TAG MWI NOTIFY received, messages are waiting ([${summaries.size}] summaries)" - ) - if (summaries.isNotEmpty()) { - val summary = summaries.first() - val label = AppUtils.getStringWithPlural( - R.plurals.mwi_messages_are_waiting, - summary.nbNew, - summary.nbNew.toString() - ) - addAlert(MWI_MESSAGES_WAITING, label) - } - } else { - Log.i("$TAG MWI NOTIFY received, no message is waiting") - removeAlert(MWI_MESSAGES_WAITING) + val address = mwi.accountAddress + val defaultAccountAddress = core.defaultAccount?.params?.identityAddress + if (defaultAccountAddress != null && address?.weakEqual(defaultAccountAddress) == true) { + parseMwiEvent(mwi) } } } @@ -394,6 +391,12 @@ class MainViewModel if (defaultAccount.state == RegistrationState.Ok && !firstAccountRegistered) { triggerNativeAddressBookImport() } + + mwiNewMessages = false + val mwi = defaultAccount.latestReceivedMessageWaitingIndication + if (mwi != null) { + parseMwiEvent(mwi) + } } } @@ -479,6 +482,10 @@ class MainViewModel askFullScreenIntentPermissionEvent.value = Event(true) } else if (!Compatibility.isPostNotificationsPermissionGranted(coreContext.context)) { askPostNotificationsPermissionEvent.value = Event(true) + } else if (mwiNewMessages) { + coreContext.postOnCoreThread { + callVoiceMail() + } } else { openDrawerEvent.value = Event(true) } @@ -715,4 +722,45 @@ class MainViewModel removeAlert(SEND_NOTIFICATIONS_PERMISSION_NOT_GRANTED) } } + + @WorkerThread + private fun parseMwiEvent(mwi: MessageWaitingIndication) { + if (mwi.hasMessageWaiting()) { + val summaries = mwi.summaries + Log.i( + "$TAG [MWI] Messages are waiting ([${summaries.size}] summaries)" + ) + if (summaries.isNotEmpty()) { + val summary = summaries.first() + val label = AppUtils.getStringWithPlural( + R.plurals.mwi_messages_are_waiting, + summary.nbNew, + summary.nbNew.toString() + ) + Log.i("$TAG [MWI] Showing alert with [${summary.nbNew}] new message(s)") + addAlert(MWI_MESSAGES_WAITING, label) + mwiNewMessages = true + } + } else { + Log.i("$TAG [MWI] No message is waiting") + removeAlert(MWI_MESSAGES_WAITING) + mwiNewMessages = false + } + } + + @WorkerThread + private fun callVoiceMail() { + val defaultAccount = LinphoneUtils.getDefaultAccount() + if (defaultAccount != null) { + val voiceMailUri = defaultAccount.params.voicemailAddress + if (voiceMailUri != null) { + Log.i("$TAG [MWI] Starting call to voicemail address [${voiceMailUri.asStringUriOnly()}]") + coreContext.startCall(voiceMailUri) + } else { + Log.e("$TAG [MWI] Can't call voicemail, no URI configured in account params!") + } + } else { + Log.e("$TAG [MWI] Can't call voicemail, no default account found!") + } + } } From d299b0b12912fd5dccebff5782b58a44ce80c9a2 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 17 Dec 2025 10:31:17 +0100 Subject: [PATCH 379/593] Added RC flag to allow disabling add contact feature --- app/src/main/assets/assistant_third_party_default_values | 3 +++ app/src/main/java/org/linphone/core/CorePreferences.kt | 7 +++++++ .../ui/main/chat/fragment/ConversationInfoFragment.kt | 2 ++ .../ui/main/chat/viewmodel/ConversationInfoViewModel.kt | 3 +++ .../ui/main/contacts/viewmodel/ContactsListViewModel.kt | 3 +++ .../linphone/ui/main/history/fragment/HistoryFragment.kt | 2 ++ .../ui/main/history/fragment/HistoryMenuDialogFragment.kt | 2 ++ app/src/main/res/layout-land/contacts_list_fragment.xml | 1 + app/src/main/res/layout/chat_info_fragment.xml | 4 ++-- .../main/res/layout/chat_participant_admin_popup_menu.xml | 5 ++++- app/src/main/res/layout/contacts_list_fragment.xml | 1 + app/src/main/res/layout/history_list_long_press_menu.xml | 5 ++++- app/src/main/res/layout/history_popup_menu.xml | 5 ++++- 13 files changed, 38 insertions(+), 5 deletions(-) diff --git a/app/src/main/assets/assistant_third_party_default_values b/app/src/main/assets/assistant_third_party_default_values index d19b3f5045..511252548d 100644 --- a/app/src/main/assets/assistant_third_party_default_values +++ b/app/src/main/assets/assistant_third_party_default_values @@ -32,4 +32,7 @@ srtp 0 +
+ 1 +
diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 51437bbbe4..60291e3297 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -264,6 +264,13 @@ class CorePreferences config.setString("app", "friend_list_to_store_newly_created_contacts", value) } + @get:AnyThread @set:WorkerThread + var disableAddContact: Boolean + get() = config.getBool("ui", "disable_add_contact", false) + set(value) { + config.setBool("ui", "disable_add_contact", value) + } + // Voice recordings related @get:AnyThread @set:WorkerThread diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt index e804acabd5..5f31052e3d 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt @@ -34,6 +34,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager +import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.core.tools.Log import org.linphone.databinding.ChatInfoFragmentBinding @@ -367,6 +368,7 @@ class ConversationInfoFragment : SlidingPaneChildFragment() { popupView.isMeAdmin = participantModel.isMyselfAdmin val friendRefKey = participantModel.refKey popupView.isParticipantContact = participantModel.friendAvailable + popupView.disableAddContact = corePreferences.disableAddContact popupView.setRemoveParticipantClickListener { Log.i("$TAG Trying to remove participant [$address]") diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt index 4ac6aadd0b..ee5f4cfbc0 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt @@ -82,6 +82,8 @@ class ConversationInfoViewModel val friendAvailable = MutableLiveData() + val disableAddContact = MutableLiveData() + val groupLeftEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -194,6 +196,7 @@ class ConversationInfoViewModel init { expandParticipants.value = true showPeerSipUri.value = false + disableAddContact.value = corePreferences.disableAddContact coreContext.postOnCoreThread { hideSipAddresses.postValue(corePreferences.hideSipAddresses) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt index 1eb63eb9f8..0e6004bb55 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt @@ -71,6 +71,8 @@ class ContactsListViewModel val showResultsLimitReached = MutableLiveData() + val disableAddContact = MutableLiveData() + val vCardTerminatedEvent: MutableLiveData>> by lazy { MutableLiveData>>() } @@ -150,6 +152,7 @@ class ContactsListViewModel fetchInProgress.value = true showFavourites.value = corePreferences.showFavoriteContacts showFilter.value = !corePreferences.hidePhoneNumbers && !corePreferences.hideSipAddresses + disableAddContact.value = corePreferences.disableAddContact coreContext.postOnCoreThread { core -> domainFilter = corePreferences.contactsFilter diff --git a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryFragment.kt b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryFragment.kt index 4fad504486..a289020d62 100644 --- a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryFragment.kt @@ -34,6 +34,7 @@ import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager +import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.core.tools.Log import org.linphone.databinding.HistoryFragmentBinding @@ -216,6 +217,7 @@ class HistoryFragment : SlidingPaneChildFragment() { popupView.contactExists = viewModel.callLogModel.value?.friendExists == true popupView.isConferenceCallLog = viewModel.isConferenceCallLog.value == true + popupView.disableAddContact = corePreferences.disableAddContact popupView.setAddToContactsListener { sharedViewModel.sipAddressToAddToNewContact = viewModel.callLogModel.value?.displayedAddress.orEmpty() diff --git a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryMenuDialogFragment.kt b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryMenuDialogFragment.kt index cab571da49..15ee5d5a5c 100644 --- a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryMenuDialogFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryMenuDialogFragment.kt @@ -29,6 +29,7 @@ import androidx.annotation.UiThread import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.databinding.HistoryListLongPressMenuBinding @UiThread @@ -69,6 +70,7 @@ class HistoryMenuDialogFragment( ): View { val view = HistoryListLongPressMenuBinding.inflate(layoutInflater) view.contactExists = contactExists + view.disableAddContact = corePreferences.disableAddContact view.setCopyNumberClickListener { onCopyNumberOrAddressToClipboard?.invoke() diff --git a/app/src/main/res/layout-land/contacts_list_fragment.xml b/app/src/main/res/layout-land/contacts_list_fragment.xml index b63594359d..b84fc34d9f 100644 --- a/app/src/main/res/layout-land/contacts_list_fragment.xml +++ b/app/src/main/res/layout-land/contacts_list_fragment.xml @@ -201,6 +201,7 @@ android:layout_margin="16dp" android:src="@drawable/user_plus" android:contentDescription="@string/content_description_contact_create" + android:visibility="@{viewModel.disableAddContact ? View.GONE : View.VISIBLE}" app:tint="?attr/color_on_main" app:backgroundTint="?attr/color_main1_500" app:shapeAppearanceOverlay="@style/rounded" diff --git a/app/src/main/res/layout/chat_info_fragment.xml b/app/src/main/res/layout/chat_info_fragment.xml index f7272d0c94..40c35a5192 100644 --- a/app/src/main/res/layout/chat_info_fragment.xml +++ b/app/src/main/res/layout/chat_info_fragment.xml @@ -469,7 +469,7 @@ android:drawableStart="@drawable/user_plus" android:onClick="@{addToContactsClickListener}" android:text="@string/conversation_info_menu_add_to_contacts" - android:visibility="@{!viewModel.isGroup && !viewModel.friendAvailable ? View.VISIBLE : View.GONE, default=gone}" + android:visibility="@{!viewModel.isGroup && !viewModel.friendAvailable && !viewModel.disableAddContact ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/action_see_contact" /> @@ -481,7 +481,7 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:background="@{!viewModel.isGroup ? @drawable/action_background_middle : @drawable/action_background_top, default=@drawable/action_background_top}" + android:background="@{viewModel.disableAddContact ? @drawable/action_background_top : !viewModel.isGroup ? @drawable/action_background_middle : @drawable/action_background_top, default=@drawable/action_background_top}" android:drawableStart="@drawable/clock_countdown" android:onClick="@{configureEphemeralMessagesClickListener}" android:text="@string/conversation_action_configure_ephemeral_messages" diff --git a/app/src/main/res/layout/chat_participant_admin_popup_menu.xml b/app/src/main/res/layout/chat_participant_admin_popup_menu.xml index 19a04b647b..dc1bb5a28a 100644 --- a/app/src/main/res/layout/chat_participant_admin_popup_menu.xml +++ b/app/src/main/res/layout/chat_participant_admin_popup_menu.xml @@ -32,6 +32,9 @@ + + + Date: Thu, 18 Dec 2025 15:37:06 +0100 Subject: [PATCH 380/593] Bumped version code --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 26cab4f8ce..dc71120ea1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -100,7 +100,7 @@ android { applicationId = packageName minSdk = 28 targetSdk = 36 - versionCode = 601001 // 6.01.001 + versionCode = 601002 // 6.01.002 versionName = "6.1.0-alpha" manifestPlaceholders["appAuthRedirectScheme"] = packageName From a7593e07fc11d6afba314f2de42bbc3b03b6e660 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 23 Dec 2025 20:28:42 +0100 Subject: [PATCH 381/593] Added a setting to edit native contacts in-app --- .idea/compiler.xml | 2 +- .idea/misc.xml | 2 +- CHANGELOG.md | 1 + .../java/org/linphone/core/CorePreferences.kt | 7 +++++ .../contacts/viewmodel/ContactViewModel.kt | 2 +- .../settings/viewmodel/SettingsViewModel.kt | 12 +++++++ app/src/main/res/layout/settings_contacts.xml | 31 ++++++++++++++++++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 9 files changed, 55 insertions(+), 4 deletions(-) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b86273d942..b589d56e9f 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index b2c751a35c..a8a2b162e3 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/CHANGELOG.md b/CHANGELOG.md index 9183cb4822..0636d2b1bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Group changes to describe their impact on the project, as follows: - one for user to choose whether to sort contacts by first name or last name - one to hide contacts that have neither a SIP address nor a phone number - one to let app auto-answer call with video sending already enabled + - one to let edit native contacts Linphone copy in-app instead of opening native addressbook third party app - Added a vu meter for recording & playback volumes (must be enabled in developer settings) - Added support for HDMI audio devices diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 60291e3297..f47a2d7e50 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -264,6 +264,13 @@ class CorePreferences config.setString("app", "friend_list_to_store_newly_created_contacts", value) } + @get:AnyThread @set:WorkerThread + var editNativeContactsInLinphone: Boolean + get() = config.getBool("ui", "edit_native_contact_in_linphone", false) + set(value) { + config.setBool("ui", "edit_native_contact_in_linphone", value) + } + @get:AnyThread @set:WorkerThread var disableAddContact: Boolean get() = config.getBool("ui", "disable_add_contact", false) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt index 9da8cd5bb1..c53e46fe40 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt @@ -352,7 +352,7 @@ class ContactViewModel coreContext.postOnCoreThread { if (::friend.isInitialized) { val uri = friend.nativeUri - if (uri != null) { + if (uri != null && !corePreferences.editNativeContactsInLinphone) { Log.i( "$TAG Contact [${friend.name}] is a native contact, opening native contact editor using URI [$uri]" ) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 2a79235a0e..3d481a515d 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -112,6 +112,8 @@ class SettingsViewModel AppUtils.getString(R.string.contact_editor_last_name), ) val sortContactsByValues = arrayListOf(0, 1) + + val editNativeContactsInLinphone = MutableLiveData() val hideEmptyContacts = MutableLiveData() val ldapAvailable = MutableLiveData() @@ -343,6 +345,7 @@ class SettingsViewModel ) sortContactsBy.postValue(if (corePreferences.sortContactsByFirstName) 0 else 1) + editNativeContactsInLinphone.postValue(corePreferences.editNativeContactsInLinphone) hideEmptyContacts.postValue(corePreferences.hideContactsWithoutPhoneNumberOrSipAddress) presenceSubscribe.postValue(core.isFriendListSubscriptionEnabled) @@ -585,6 +588,15 @@ class SettingsViewModel } } + @UiThread + fun toggleEditNativeContactsInLinphone() { + val newValue = editNativeContactsInLinphone.value == false + coreContext.postOnCoreThread { + corePreferences.editNativeContactsInLinphone = newValue + editNativeContactsInLinphone.postValue(newValue) + } + } + @UiThread fun toggleHideEmptyContacts() { val newValue = hideEmptyContacts.value == false diff --git a/app/src/main/res/layout/settings_contacts.xml b/app/src/main/res/layout/settings_contacts.xml index d381c4d738..ee8d39babc 100644 --- a/app/src/main/res/layout/settings_contacts.xml +++ b/app/src/main/res/layout/settings_contacts.xml @@ -78,6 +78,35 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toStartOf="@id/hide_empty_contacts_switch"/> + + + + + app:layout_constraintTop_toBottomOf="@id/edit_native_contacts_in_linphone_switch" /> Marquer la conversation comme lue lorsqu\'une notification de message est supprimée Contacts Trier les contacts par + Editer les contacts natifs dans &appName; Masquer les contacts sans adresse SIP ni numéro de téléphone Souscrire aux informations de présence Ajouter un serveur LDAP diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index af4aaff6d8..5fb0c16684 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -268,6 +268,7 @@ Mark conversation as read when dismissing message notification Contacts Sort contacts by + Use &appName; contact editor for native contacts Hide contacts without SIP address nor phone number Subscribe to presence info Add LDAP server From 6f09853424eaacaa9f454fa5be6900e8501932ef Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 5 Jan 2026 10:40:35 +0100 Subject: [PATCH 382/593] Fixed attaching file to conversation from third party app using shortcut if matching conversation is already displayed --- .../org/linphone/ui/main/chat/fragment/ConversationFragment.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 7de0e4b6fb..a79f34a11e 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -509,6 +509,8 @@ open class ConversationFragment : SlidingPaneChildFragment() { ) } } else { + sharedViewModel.displayedChatRoom = viewModel.chatRoom + sendMessageViewModel.configureChatRoom(viewModel.chatRoom) adapter.setIsConversationSecured(viewModel.isEndToEndEncrypted.value == true) From 24d808b1a78fe6835fb5eca8f68db4bec726b23c Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 5 Jan 2026 11:11:26 +0100 Subject: [PATCH 383/593] Hide presence SIP addresses from contact details & editor views --- CHANGELOG.md | 1 + .../org/linphone/contacts/ContactsManager.kt | 18 ++++-------------- .../viewmodel/ContactNewOrEditViewModel.kt | 18 ++++++++++++++---- .../java/org/linphone/utils/LinphoneUtils.kt | 15 +++++++++++++++ 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0636d2b1bf..ef058c340c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ Group changes to describe their impact on the project, as follows: - No longer follow TelecomManager audio endpoint during calls, using our own routing policy - Removing an account will also remove all related data in the local database (auth info, call logs, conversations, meetings, etc...) - Hide SIP address/phone number picker dialog if contact has exactly one SIP address matching both the app default domain & the currently selected account domain +- Hide SIP address associated to phone number through presence mecanism in contact details & editor views. - Improved UI on tablets with screen sw600dp and higher, will look more like our desktop app - Improved navigation within app when using a keyboard - Now loading media/documents contents in conversation by chunks (instead of all of them at once) diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 33625bfc58..3902b2707d 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -817,6 +817,10 @@ fun Friend.getListOfSipAddressesAndPhoneNumbers(listener: ContactNumberOrAddress // Will return an empty list if corePreferences.hideSipAddresses == true for (address in getListOfSipAddresses()) { + if (LinphoneUtils.isSipAddressLinkedToPhoneNumberByPresence(this, address.asStringUriOnly())) { + continue + } + val data = ContactNumberOrAddressModel( this, address, @@ -832,7 +836,6 @@ fun Friend.getListOfSipAddressesAndPhoneNumbers(listener: ContactNumberOrAddress return addressesAndNumbers } - val indexOfLastSipAddress = addressesAndNumbers.count() for (number in phoneNumbersWithLabel) { val phoneNumber = number.phoneNumber val presenceModel = getPresenceModelForUriOrTel(phoneNumber) @@ -840,25 +843,12 @@ fun Friend.getListOfSipAddressesAndPhoneNumbers(listener: ContactNumberOrAddress var presenceAddress: Address? = null if (presenceModel != null && hasPresenceInfo) { - // Show linked SIP address if not already stored as-is val contact = presenceModel.contact if (!contact.isNullOrEmpty()) { val address = core.interpretUrl(contact, false) if (address != null) { address.clean() // To remove ;user=phone presenceAddress = address - if (!corePreferences.hideSipAddresses && addressesAndNumbers.find { it.address?.weakEqual(address) == true } == null) { - val data = ContactNumberOrAddressModel( - this, - address, - address.asStringUriOnly(), - true, // SIP addresses are always enabled - listener, - true, - hasPresence = true - ) - addressesAndNumbers.add(indexOfLastSipAddress, data) - } } else { Log.e("[Contacts Manager] Failed to parse phone number [$phoneNumber] contact address [$contact] from presence model!") } diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt index b78d170040..11284139c0 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt @@ -42,6 +42,7 @@ import org.linphone.ui.main.contacts.model.NewOrEditNumberOrAddressModel import org.linphone.utils.Event import org.linphone.utils.FileUtils import androidx.core.net.toUri +import org.linphone.utils.LinphoneUtils class ContactNewOrEditViewModel @UiThread @@ -114,8 +115,13 @@ class ContactNewOrEditViewModel } for (address in friend.addresses) { - addSipAddress(address.asStringUriOnly()) + val sipAddress = address.asStringUriOnly() + // Prevents showing presence address as editable when in fact it's not + if (!LinphoneUtils.isSipAddressLinkedToPhoneNumberByPresence(friend, sipAddress)) { + addSipAddress(sipAddress) + } } + for (number in friend.phoneNumbersWithLabel) { addPhoneNumber(number.phoneNumber, number.label) } @@ -341,16 +347,20 @@ class ContactNewOrEditViewModel if (jobTitle.value.orEmpty() != friend.jobTitle.orEmpty()) return true for (address in friend.addresses) { + val sipAddress = address.asStringUriOnly() + if (LinphoneUtils.isSipAddressLinkedToPhoneNumberByPresence(friend, sipAddress)) continue + val found = sipAddresses.find { - it.isSip && it.value.value.orEmpty() == address.asStringUriOnly() + it.isSip && it.value.value.orEmpty() == sipAddress } if (found == null) return true } for (address in sipAddresses) { - if (address.value.value.orEmpty().isEmpty()) continue + val sipAddress = address.value.value.orEmpty() + if (sipAddress.isEmpty()) continue val found = friend.addresses.find { - it.asStringUriOnly() == address.value.value.orEmpty() + it.asStringUriOnly() == sipAddress } if (found == null) return true } diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index a125feb4a9..73afcec004 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -217,6 +217,21 @@ class LinphoneUtils { return null } + @WorkerThread + fun isSipAddressLinkedToPhoneNumberByPresence(friend: Friend, sipAddress: String): Boolean { + for (phoneNumber in friend.phoneNumbers) { + val presenceModel = friend.getPresenceModelForUriOrTel(phoneNumber) + if (presenceModel != null) { + val contact = presenceModel.contact + if (contact == sipAddress) { + Log.i("$TAG SIP address [$sipAddress] is presence contact address for phone number [$phoneNumber]") + return true + } + } + } + return false + } + @AnyThread fun isCallIncoming(callState: Call.State): Boolean { return when (callState) { From b88b6a80933c2466098717981597b989e5e7709d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 5 Jan 2026 14:19:08 +0100 Subject: [PATCH 384/593] Fixed hidden reply area when adding file to send area --- CHANGELOG.md | 4 ++-- app/src/main/res/layout/chat_conversation_send_area.xml | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef058c340c..78544e9c88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,7 +76,7 @@ Group changes to describe their impact on the project, as follows: ### Changed - Added shrink resources to release config in gradle -### Fixed +### Fixed - Remove AuthInfo when configuring a CardDAV friend list if synchronization fails - Added missing toast when starting a group call or meeting if there's an issue - Fixed crash in RecordingPlayerFragment due to used lateinit property before it's initialized @@ -357,7 +357,7 @@ Group changes to describe their impact on the project, as follows: - Reverted the way of playing incoming call ringone (you may have to configure your own ringtone again), was causing various issues depending on devices/firmwares - Show all call history entries if only one account is configured (workaround for missing history for now until a proper fix will be done in SDK) -### Fixed +### Fixed - Issue preventing bluetooth Hearing Aids from working properly (and fixed earpiece/hearing aids icon) - Prevent Qr Code scanner to use static picture camera - Prevent user from connecting the same account multiple times diff --git a/app/src/main/res/layout/chat_conversation_send_area.xml b/app/src/main/res/layout/chat_conversation_send_area.xml index 35c28d99aa..cc315ee96f 100644 --- a/app/src/main/res/layout/chat_conversation_send_area.xml +++ b/app/src/main/res/layout/chat_conversation_send_area.xml @@ -35,6 +35,8 @@ @@ -42,6 +44,8 @@ From 50aa053c1957817577742feb56278bbc2a0827b5 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 5 Jan 2026 14:55:03 +0100 Subject: [PATCH 385/593] Filter participants list using user input after '@' --- CHANGELOG.md | 4 ++ .../chat/fragment/ConversationFragment.kt | 21 ++++++--- .../ui/main/chat/model/MessageModel.kt | 20 ++++++--- .../ChatMessageLongPressViewModel.kt | 2 +- .../SendMessageInConversationViewModel.kt | 44 +++++++++++++++++-- .../chat_conversation_participants_area.xml | 26 ++++++++++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 8 files changed, 102 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78544e9c88..754f2837f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Group changes to describe their impact on the project, as follows: - Support right click on some items to open bottom sheet/menu - Added toggle speaker action in active call notification - Increased text size for chat messages that only contains emoji(s) +- Use user-input to filter participants list after typing "@" in conversation send area - Handle read-only CardDAV address books, disable edit/delete menus for contacts in read-only FriendList - Added swipe/pull to refresh on contacts list of a CardDAV addressbook has been configured to force the synchronization - Show information to user when filtering contacts doesn't show them all and user may have to refine it's search @@ -52,6 +53,9 @@ Group changes to describe their impact on the project, as follows: - Added more info into StartupListener logs - Updated password forgotten procedure, will use online account manager platform +### Fixed +- Copy raw message content instead of modified one when it contains a participant mention ("@username") + ## [6.0.21] - 2025-12-16 ### Added diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index a79f34a11e..aa18972e6e 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -283,13 +283,22 @@ open class ConversationFragment : SlidingPaneChildFragment() { override fun afterTextChanged(editable: Editable?) { if (viewModel.isGroup.value == true) { - sendMessageViewModel.closeParticipantsList() - val split = editable.toString().split(" ") - for (part in split) { - if (part == "@") { - Log.i("$TAG '@' found, opening participants list") - sendMessageViewModel.openParticipantsList() + if (split.isNotEmpty()) { + val lastPart = split.last() + if (lastPart.isNotEmpty() && lastPart.startsWith("@")) { + coreContext.postOnCoreThread { + val filter = if (lastPart.length > 1) lastPart.substring(1) else "" + sendMessageViewModel.filterParticipantsList(filter) + } + + if (sendMessageViewModel.isParticipantsListOpen.value == false) { + Log.i("$TAG '@' found, opening participants list") + sendMessageViewModel.openParticipantsList() + } + } else if (sendMessageViewModel.isParticipantsListOpen.value == true) { + Log.i("$TAG Closing participants list") + sendMessageViewModel.closeParticipantsList() } } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt index 99cad6b9b7..310350dfa9 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt @@ -25,6 +25,7 @@ import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned import android.text.style.StyleSpan +import androidx.annotation.AnyThread import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.lifecycle.MediatorLiveData @@ -151,6 +152,8 @@ class MessageModel val isSelected = MutableLiveData() + private var rawTextContent: String = "" + // Below are for conferences info val meetingFound = MutableLiveData() @@ -436,6 +439,11 @@ class MessageModel avatarModel.postValue(avatar) } + @AnyThread + fun getRawTextContent(): String { + return rawTextContent + } + @WorkerThread private fun computeContentsList() { Log.d("$TAG Computing message contents list") @@ -686,10 +694,10 @@ class MessageModel @WorkerThread private fun computeTextContent(content: Content, highlight: String) { - val textContent = content.utf8Text.orEmpty().trim() - val spannableBuilder = SpannableStringBuilder(textContent) + rawTextContent = content.utf8Text.orEmpty().trim() + val spannableBuilder = SpannableStringBuilder(rawTextContent) - val emojiOnly = AppUtils.isTextOnlyContainsEmoji(textContent) + val emojiOnly = AppUtils.isTextOnlyContainsEmoji(rawTextContent) isTextEmoji.postValue(emojiOnly) if (emojiOnly) { text.postValue(spannableBuilder) @@ -698,7 +706,7 @@ class MessageModel // Check for search if (highlight.isNotEmpty()) { - val indexStart = textContent.indexOf(highlight, 0, ignoreCase = true) + val indexStart = rawTextContent.indexOf(highlight, 0, ignoreCase = true) if (indexStart >= 0) { isTextHighlighted = true val indexEnd = indexStart + highlight.length @@ -713,12 +721,12 @@ class MessageModel // Check for mentions val chatRoom = chatMessage.chatRoom - val matcher = Pattern.compile(MENTION_REGEXP).matcher(textContent) + val matcher = Pattern.compile(MENTION_REGEXP).matcher(rawTextContent) var offset = 0 while (matcher.find()) { val start = matcher.start() val end = matcher.end() - val source = textContent.subSequence(start + 1, end) // +1 to remove @ + val source = rawTextContent.subSequence(start + 1, end) // +1 to remove @ Log.d("$TAG Found mention [$source]") // Find address matching username diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ChatMessageLongPressViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ChatMessageLongPressViewModel.kt index f6957b3eb3..56af87113d 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ChatMessageLongPressViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ChatMessageLongPressViewModel.kt @@ -150,7 +150,7 @@ class ChatMessageLongPressViewModel : GenericViewModel() { fun copyClickListener() { Log.i("$TAG Copying message text into clipboard") - val text = messageModel.value?.text?.value?.toString() + val text = messageModel.value?.getRawTextContent() val clipboard = coreContext.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val label = "Message" clipboard.setPrimaryClip(ClipData.newPlainText(label, text)) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt index 4b72bf66f4..283b13fd26 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt @@ -110,6 +110,8 @@ class SendMessageInConversationViewModel val voiceRecordPlayerPosition = MutableLiveData() + val isComputingParticipantsList = MutableLiveData() + private lateinit var voiceRecordPlayer: Player private val playerListener = PlayerListener { @@ -143,6 +145,8 @@ class SendMessageInConversationViewModel private var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null + private var participantsListFilter = "" + private val chatRoomListener = object : ChatRoomListenerStub() { @WorkerThread override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) { @@ -163,6 +167,7 @@ class SendMessageInConversationViewModel isKeyboardOpen.value = false isEmojiPickerOpen.value = false areFilePickersOpen.value = false + isParticipantsListOpen.value = false isVoiceRecording.value = false isPlayingVoiceRecord.value = false isCallConversation.value = false @@ -409,6 +414,10 @@ class SendMessageInConversationViewModel @UiThread fun closeParticipantsList() { isParticipantsListOpen.value = false + coreContext.postOnCoreThread { + participantsListFilter = "" + computeParticipantsList() + } } @UiThread @@ -571,7 +580,32 @@ class SendMessageInConversationViewModel } @WorkerThread - private fun computeParticipantsList() { + fun filterParticipantsList(filter: String) { + Log.i("$TAG Filtering participants list using user-input [$filter]") + if (filter.isEmpty() && participantsListFilter.isNotEmpty()) { + participantsListFilter = "" + computeParticipantsList() + return + } + + if (filter.length >= participantsListFilter.length) { + isComputingParticipantsList.postValue(true) + participantsListFilter = filter + val currentList = participants.value.orEmpty() + val newList = currentList.filter { + it.address.asStringUriOnly().contains(filter) || it.avatarModel.contactName?.contains(filter) == true + } + participants.postValue(newList as ArrayList) + isComputingParticipantsList.postValue(false) + } else { + participantsListFilter = filter + computeParticipantsList(filter) + } + } + + @WorkerThread + private fun computeParticipantsList(filter: String = "") { + isComputingParticipantsList.postValue(true) val participantsList = arrayListOf() for (participant in chatRoom.participants) { @@ -580,14 +614,18 @@ class SendMessageInConversationViewModel coreContext.postOnCoreThread { val username = clicked.address.username if (!username.isNullOrEmpty()) { - participantUsernameToAddEvent.postValue(Event(username)) + participantUsernameToAddEvent.postValue(Event(username.substring(participantsListFilter.length))) } } }) - participantsList.add(model) + + if (filter.isEmpty() || participant.address.asStringUriOnly().contains(filter) || model.avatarModel.contactName?.contains(filter) == true) { + participantsList.add(model) + } } participants.postValue(participantsList) + isComputingParticipantsList.postValue(false) } @WorkerThread diff --git a/app/src/main/res/layout/chat_conversation_participants_area.xml b/app/src/main/res/layout/chat_conversation_participants_area.xml index 824ea77f04..f09e155931 100644 --- a/app/src/main/res/layout/chat_conversation_participants_area.xml +++ b/app/src/main/res/layout/chat_conversation_participants_area.xml @@ -22,6 +22,18 @@ android:importantForAccessibility="no" app:layout_constraintTop_toTopOf="parent"/> + + + app:layout_constraintTop_toBottomOf="@id/participants_header"> + + Pour tout le monde Le message a été supprimé Vous avez supprimé le message + Participants Participants (%s) Ajouter des participants diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5fb0c16684..dcf1d2cd02 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -609,6 +609,7 @@ For everyone This message has been deleted You have deleted this message + Participants Group members (%s) Add participants From a897c127e59c14c82716a6538e3f7965eca97883 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 5 Jan 2026 15:58:18 +0100 Subject: [PATCH 386/593] Using onFileTransferTerminated instead of relying on FileTransferDone chat message state --- .../ui/main/chat/model/MessageModel.kt | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt index 310350dfa9..8d0ca76e9a 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt @@ -219,27 +219,13 @@ class MessageModel private val chatMessageListener = object : ChatMessageListenerStub() { @WorkerThread override fun onMsgStateChanged(message: ChatMessage, messageState: ChatMessage.State?) { + Log.i("$TAG Chat message [${message.messageId}] state changed to [$messageState]") if (messageState != ChatMessage.State.FileTransferDone && messageState != ChatMessage.State.FileTransferInProgress) { statusIcon.postValue(LinphoneUtils.getChatIconResId(chatMessage.state)) if (messageState == ChatMessage.State.Displayed) { isRead = chatMessage.isRead } - } else if (messageState == ChatMessage.State.FileTransferDone) { - Log.i("$TAG File transfer is done") - transferringFileModel?.updateTransferProgress(-1) - transferringFileModel = null - if (!allFilesDownloaded) { - computeContentsList() - } else { - for (content in message.contents) { - if (content.isVoiceRecording) { - Log.i("$TAG File transfer done, updating voice record info") - computeVoiceRecordContent(content) - break - } - } - } } isInError.postValue(messageState == ChatMessage.State.NotDelivered) } @@ -247,22 +233,7 @@ class MessageModel @WorkerThread override fun onFileTransferTerminated(message: ChatMessage, content: Content) { Log.i("$TAG File [${content.name}] from message [${message.messageId}] transfer terminated") - - // Never do auto media export for ephemeral messages! - if (corePreferences.makePublicMediaFilesDownloaded && !message.isEphemeral) { - val path = content.filePath - if (path.isNullOrEmpty()) return - - val mime = "${content.type}/${content.subtype}" - val mimeType = FileUtils.getMimeType(mime) - when (mimeType) { - FileUtils.MimeType.Image, FileUtils.MimeType.Video, FileUtils.MimeType.Audio -> { - Log.i("$TAG Exporting file path [$path] to the native media gallery") - onFileToExportToNativeGallery?.invoke(path) - } - else -> {} - } - } + fileTransferTerminated(message, content) } @WorkerThread @@ -1067,4 +1038,37 @@ class MessageModel "$TAG Found voice record with path [$voiceRecordPath] and duration [$formattedDuration]" ) } + + @WorkerThread + private fun fileTransferTerminated(message: ChatMessage, content: Content) { + // Never do auto media export for ephemeral messages! + if (corePreferences.makePublicMediaFilesDownloaded && !message.isEphemeral) { + val path = content.filePath + if (path.isNullOrEmpty()) return + + val mime = "${content.type}/${content.subtype}" + val mimeType = FileUtils.getMimeType(mime) + when (mimeType) { + FileUtils.MimeType.Image, FileUtils.MimeType.Video, FileUtils.MimeType.Audio -> { + Log.i("$TAG Exporting file path [$path] to the native media gallery") + onFileToExportToNativeGallery?.invoke(path) + } + else -> {} + } + } + + transferringFileModel?.updateTransferProgress(-1) + transferringFileModel = null + if (!allFilesDownloaded) { + computeContentsList() + } else { + for (content in message.contents) { + if (content.isVoiceRecording) { + Log.i("$TAG File transfer done, updating voice record info") + computeVoiceRecordContent(content) + break + } + } + } + } } From 7e0353cc9183a1508029a349352d3c4634e79c20 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 5 Jan 2026 16:58:08 +0100 Subject: [PATCH 387/593] Prevent participants list from blinking if it hasn't changed, using RecyclerView will also improve performances a bit for conversations with a lot of participants --- .../chat/fragment/ConversationFragment.kt | 16 +++++++++++++ .../SendMessageInConversationViewModel.kt | 2 +- .../chat_conversation_participants_area.xml | 24 +++++++------------ app/src/main/res/values/dimen.xml | 2 +- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index aa18972e6e..5ff131e7dd 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -94,6 +94,7 @@ import org.linphone.utils.hideKeyboard import org.linphone.utils.setKeyboardInsetListener import org.linphone.utils.showKeyboard import androidx.core.net.toUri +import org.linphone.ui.main.chat.adapter.ConversationParticipantsAdapter import org.linphone.ui.main.chat.model.MessageDeleteDialogModel @UiThread @@ -114,6 +115,8 @@ open class ConversationFragment : SlidingPaneChildFragment() { private lateinit var adapter: ConversationEventAdapter + private lateinit var participantsAdapter: ConversationParticipantsAdapter + private lateinit var bottomSheetAdapter: MessageBottomSheetAdapter private val args: ConversationFragmentArgs by navArgs() @@ -395,6 +398,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { super.onCreate(savedInstanceState) adapter = ConversationEventAdapter() + participantsAdapter = ConversationParticipantsAdapter() headerItemDecoration = RecyclerViewHeaderDecoration( requireContext(), adapter, @@ -466,6 +470,10 @@ open class ConversationFragment : SlidingPaneChildFragment() { layoutManager.stackFromEnd = true binding.eventsList.layoutManager = layoutManager + binding.sendArea.participants.participants.setHasFixedSize(true) + val participantsLayoutManager = LinearLayoutManager(requireContext()) + binding.sendArea.participants.participants.layoutManager = participantsLayoutManager + val callbacks = RecyclerViewSwipeUtilsCallback( R.drawable.reply, ConversationEventAdapter.EventViewHolder::class.java @@ -764,6 +772,14 @@ open class ConversationFragment : SlidingPaneChildFragment() { } } + sendMessageViewModel.participants.observe(viewLifecycleOwner) { + participantsAdapter.submitList(it) + + if (binding.sendArea.participants.participants.adapter != participantsAdapter) { + binding.sendArea.participants.participants.adapter = participantsAdapter + } + } + viewModel.focusSearchBarEvent.observe(viewLifecycleOwner) { it.consume { show -> if (show) { diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt index 283b13fd26..0b28e186f0 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt @@ -593,7 +593,7 @@ class SendMessageInConversationViewModel participantsListFilter = filter val currentList = participants.value.orEmpty() val newList = currentList.filter { - it.address.asStringUriOnly().contains(filter) || it.avatarModel.contactName?.contains(filter) == true + it.address.username.orEmpty().contains(filter) || it.avatarModel.contactName?.contains(filter) == true } participants.postValue(newList as ArrayList) isComputingParticipantsList.postValue(false) diff --git a/app/src/main/res/layout/chat_conversation_participants_area.xml b/app/src/main/res/layout/chat_conversation_participants_area.xml index f09e155931..7162592add 100644 --- a/app/src/main/res/layout/chat_conversation_participants_area.xml +++ b/app/src/main/res/layout/chat_conversation_participants_area.xml @@ -31,27 +31,19 @@ android:text="@string/conversation_participants_list_header" android:textSize="12sp" android:textColor="?attr/color_main2_500" - app:layout_constraintTop_toBottomOf="@id/participants_separator" + app:layout_constraintTop_toTopOf="@id/participants_close" + app:layout_constraintBottom_toBottomOf="@id/participants_close" app:layout_constraintStart_toStartOf="parent" /> - - - - - + app:layout_constraintTop_toBottomOf="@id/participants_close" /> 10dp 300dp - 300dp + 200dp 425dp 15dp From fe788caf0e01185f31cb161d2146099af8675f6f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 6 Jan 2026 12:16:10 +0100 Subject: [PATCH 388/593] Added label when no participant match user input after typing @ in a group conversation --- .../chat_conversation_participants_area.xml | 15 +++++++++++++++ app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 3 files changed, 17 insertions(+) diff --git a/app/src/main/res/layout/chat_conversation_participants_area.xml b/app/src/main/res/layout/chat_conversation_participants_area.xml index 7162592add..cfb8c21f52 100644 --- a/app/src/main/res/layout/chat_conversation_participants_area.xml +++ b/app/src/main/res/layout/chat_conversation_participants_area.xml @@ -45,6 +45,21 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/participants_close" /> + + Le message a été supprimé Vous avez supprimé le message Participants + Aucun participant trouvé Participants (%s) Ajouter des participants diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dcf1d2cd02..bca17f369a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -610,6 +610,7 @@ This message has been deleted You have deleted this message Participants + No participants found Group members (%s) Add participants From 9151898a4dddaabe0ecb4da5eb22b55cf10d179c Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 6 Jan 2026 16:09:47 +0100 Subject: [PATCH 389/593] Disable search direction after latest matching result found --- .../chat/viewmodel/ConversationViewModel.kt | 28 +++++++++++++++---- .../res/layout/chat_conversation_fragment.xml | 2 +- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt index ea71e1f1cf..73851eedb1 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt @@ -98,6 +98,8 @@ class ConversationViewModel val canSearchDown = MutableLiveData() + val canSearchUp = MutableLiveData() + val itemToScrollTo = MutableLiveData() val isUserScrollingUp = MutableLiveData() @@ -351,6 +353,7 @@ class ConversationViewModel isDisabledBecauseNotSecured.value = false searchInProgress.value = false canSearchDown.value = false + canSearchUp.value = false itemToScrollTo.value = -1 } @@ -382,6 +385,7 @@ class ConversationViewModel @UiThread fun openSearchBar() { + canSearchUp.value = true searchBarVisible.value = true focusSearchBarEvent.value = Event(true) } @@ -393,6 +397,7 @@ class ConversationViewModel focusSearchBarEvent.value = Event(false) latestMatch = null canSearchDown.value = false + canSearchUp.value = false coreContext.postOnCoreThread { for (eventLog in eventsList) { @@ -405,15 +410,19 @@ class ConversationViewModel @UiThread fun searchUp() { - coreContext.postOnCoreThread { - searchChatMessage(SearchDirection.Up) + if (canSearchUp.value == true) { + coreContext.postOnCoreThread { + searchChatMessage(SearchDirection.Up) + } } } @UiThread fun searchDown() { - coreContext.postOnCoreThread { - searchChatMessage(SearchDirection.Down) + if (canSearchDown.value == true) { + coreContext.postOnCoreThread { + searchChatMessage(SearchDirection.Down) + } } } @@ -1007,10 +1016,19 @@ class ConversationViewModel val index = eventsList.indexOf(found) itemToScrollTo.postValue(index) } + // Disable button as latest result has been reached + if (direction == SearchDirection.Down) { + canSearchDown.postValue(false) + } else { + canSearchUp.postValue(false) + } R.string.conversation_search_no_more_match } showRedToast(message, R.drawable.magnifying_glass) } else { + canSearchDown.postValue(true) + canSearchUp.postValue(true) + Log.i( "$TAG Found result [${match.chatMessage?.messageId}] while looking up for message with text [$textToSearch] in direction [$direction] starting from message [${latestMatch?.chatMessage?.messageId}]" ) @@ -1029,8 +1047,6 @@ class ConversationViewModel itemToScrollTo.postValue(index) searchInProgress.postValue(false) } - - canSearchDown.postValue(true) } } diff --git a/app/src/main/res/layout/chat_conversation_fragment.xml b/app/src/main/res/layout/chat_conversation_fragment.xml index 580039cfc9..8a58a9c2d7 100644 --- a/app/src/main/res/layout/chat_conversation_fragment.xml +++ b/app/src/main/res/layout/chat_conversation_fragment.xml @@ -271,7 +271,7 @@ style="@style/icon_top_bar_button_style" android:id="@+id/search_up" android:onClick="@{() -> viewModel.searchUp()}" - android:enabled="@{viewModel.searchFilter.length() > 0}" + android:enabled="@{viewModel.searchFilter.length() > 0 && viewModel.canSearchUp}" android:layout_width="wrap_content" android:layout_height="0dp" android:src="@drawable/caret_up" From d90861b5f3c32d6bbf552d0abd3b58956dcd96cf Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 6 Jan 2026 16:20:00 +0100 Subject: [PATCH 390/593] Cancel search when making reply/edit/forward action on a message --- .../ui/main/chat/fragment/ConversationFragment.kt | 14 ++++++++++---- .../layout/chat_conversation_participants_area.xml | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 5ff131e7dd..f29e9ae6cb 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -470,9 +470,9 @@ open class ConversationFragment : SlidingPaneChildFragment() { layoutManager.stackFromEnd = true binding.eventsList.layoutManager = layoutManager - binding.sendArea.participants.participants.setHasFixedSize(true) + binding.sendArea.participants.participantsList.setHasFixedSize(true) val participantsLayoutManager = LinearLayoutManager(requireContext()) - binding.sendArea.participants.participants.layoutManager = participantsLayoutManager + binding.sendArea.participants.participantsList.layoutManager = participantsLayoutManager val callbacks = RecyclerViewSwipeUtilsCallback( R.drawable.reply, @@ -494,6 +494,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { if (chatMessageModel.hasBeenRetracted.value == true) { // Don't allow to reply to retracted messages // TODO: notify user? } else { + viewModel.closeSearchBar() sendMessageViewModel.replyToMessage(chatMessageModel) // Open keyboard & focus edit text binding.sendArea.messageToSend.showKeyboard() @@ -775,8 +776,8 @@ open class ConversationFragment : SlidingPaneChildFragment() { sendMessageViewModel.participants.observe(viewLifecycleOwner) { participantsAdapter.submitList(it) - if (binding.sendArea.participants.participants.adapter != participantsAdapter) { - binding.sendArea.participants.participants.adapter = participantsAdapter + if (binding.sendArea.participants.participantsList.adapter != participantsAdapter) { + binding.sendArea.participants.participantsList.adapter = participantsAdapter } } @@ -896,6 +897,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { it.consume { val model = messageLongPressViewModel.messageModel.value if (model != null) { + viewModel.closeSearchBar() sendMessageViewModel.editMessage(model) // Open keyboard & focus edit text @@ -912,6 +914,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { it.consume { val model = messageLongPressViewModel.messageModel.value if (model != null) { + viewModel.closeSearchBar() sendMessageViewModel.replyToMessage(model) // Open keyboard & focus edit text binding.sendArea.messageToSend.showKeyboard() @@ -938,6 +941,9 @@ open class ConversationFragment : SlidingPaneChildFragment() { it.consume { val model = messageLongPressViewModel.messageModel.value if (model != null) { + viewModel.closeSearchBar() + sendMessageViewModel.cancelReply() + // Remove observer before setting the message to forward // as we don't want to forward it in this chat room sharedViewModel.messageToForwardEvent.removeObservers(viewLifecycleOwner) diff --git a/app/src/main/res/layout/chat_conversation_participants_area.xml b/app/src/main/res/layout/chat_conversation_participants_area.xml index cfb8c21f52..86e353e046 100644 --- a/app/src/main/res/layout/chat_conversation_participants_area.xml +++ b/app/src/main/res/layout/chat_conversation_participants_area.xml @@ -36,7 +36,7 @@ app:layout_constraintStart_toStartOf="parent" /> Date: Tue, 6 Jan 2026 17:29:15 +0100 Subject: [PATCH 391/593] Fixed layout broken by recyclerview id change --- .../layout/chat_conversation_participants_area.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/layout/chat_conversation_participants_area.xml b/app/src/main/res/layout/chat_conversation_participants_area.xml index 86e353e046..6984348370 100644 --- a/app/src/main/res/layout/chat_conversation_participants_area.xml +++ b/app/src/main/res/layout/chat_conversation_participants_area.xml @@ -55,10 +55,10 @@ android:textSize="14sp" android:textColor="?attr/color_main2_500" android:visibility="@{viewModel.participants.empty ? View.VISIBLE : View.GONE, default=gone}" - app:layout_constraintTop_toTopOf="@id/participants" - app:layout_constraintBottom_toBottomOf="@id/participants" - app:layout_constraintStart_toStartOf="@id/participants" - app:layout_constraintEnd_toEndOf="@id/participants"/> + app:layout_constraintTop_toTopOf="@id/participants_list" + app:layout_constraintBottom_toBottomOf="@id/participants_list" + app:layout_constraintStart_toStartOf="@id/participants_list" + app:layout_constraintEnd_toEndOf="@id/participants_list"/> + app:layout_constraintTop_toTopOf="@id/participants_list" + app:layout_constraintBottom_toBottomOf="@id/participants_list" /> Date: Thu, 8 Jan 2026 09:54:25 +0100 Subject: [PATCH 392/593] Do not rely on chatMessageSending callback for scenario where message(s) are queued because of delayed subscribe --- .../org/linphone/contacts/ContactsManager.kt | 3 +++ .../chat/fragment/ConversationFragment.kt | 6 +++++ .../chat/viewmodel/ConversationViewModel.kt | 26 ++++++++++--------- .../SendMessageInConversationViewModel.kt | 7 +++++ 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 3902b2707d..14b07cb680 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -63,6 +63,7 @@ import org.linphone.utils.ImageUtils import org.linphone.utils.LinphoneUtils import org.linphone.utils.PhoneNumberUtils import org.linphone.utils.ShortcutUtils +import java.io.FileNotFoundException class ContactsManager @UiThread @@ -741,6 +742,8 @@ fun Friend.getNativeContactPictureUri(): Uri? { fd.close() return pictureUri } + } catch (fnfe: FileNotFoundException) { + Log.w("[Contacts Manager] Can't open [$pictureUri] for contact [$name]: $fnfe") } catch (e: Exception) { Log.e("[Contacts Manager] Can't open [$pictureUri] for contact [$name]: $e") } diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index f29e9ae6cb..1258064420 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -740,6 +740,12 @@ open class ConversationFragment : SlidingPaneChildFragment() { false } + sendMessageViewModel.messageSentEvent.observe(viewLifecycleOwner) { + it.consume { message -> + viewModel.addSentMessageToEventsList(message) + } + } + sendMessageViewModel.emojiToAddEvent.observe(viewLifecycleOwner) { it.consume { emoji -> binding.sendArea.messageToSend.addCharacterAtPosition(emoji) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt index 73851eedb1..45c41a9513 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt @@ -196,17 +196,6 @@ class ConversationViewModel Log.i("$TAG Conversation was marked as read") } - @WorkerThread - override fun onChatMessageSending(chatRoom: ChatRoom, eventLog: EventLog) { - val message = eventLog.chatMessage - Log.i("$TAG Message [$message] is being sent, marking conversation as read") - - // Prevents auto scroll to go to latest received message - chatRoom.markAsRead() - - addEvents(arrayOf(eventLog)) - } - @WorkerThread override fun onChatMessagesReceived(chatRoom: ChatRoom, eventLogs: Array) { Log.i("$TAG Received [${eventLogs.size}] new message(s)") @@ -622,6 +611,19 @@ class ConversationViewModel } } + @UiThread + fun addSentMessageToEventsList(message: ChatMessage) { + coreContext.postOnCoreThread { + val eventLog = message.eventLog + if (eventLog != null) { + Log.i("$TAG Adding sent message with ID [${message.messageId}] to events list") + addEvents(arrayOf(eventLog)) + } else { + Log.e("$TAG Failed to get event log for sent message with ID [${message.messageId}]") + } + } + } + @WorkerThread private fun configureChatRoom() { if (!isChatRoomInitialized()) return @@ -718,7 +720,7 @@ class ConversationViewModel @WorkerThread private fun addEvents(eventLogs: Array) { - Log.i("$TAG Adding [${eventLogs.size}] events") + Log.i("$TAG Adding [${eventLogs.size}] event(s)") // Need to use a new list, otherwise ConversationFragment's dataObserver isn't triggered... val list = arrayListOf() list.addAll(eventsList) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt index 0b28e186f0..f44cb2a5df 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt @@ -135,6 +135,10 @@ class SendMessageInConversationViewModel MutableLiveData>() } + val messageSentEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + lateinit var chatRoom: ChatRoom private var chatMessageToReplyTo: ChatMessage? = null @@ -325,6 +329,7 @@ class SendMessageInConversationViewModel val voiceMessage = chatRoom.createEmptyMessage() voiceMessage.addContent(content) voiceMessage.send() + messageSentEvent.postValue(Event(voiceMessage)) } else { message.addContent(content) } @@ -356,6 +361,7 @@ class SendMessageInConversationViewModel val fileMessage = chatRoom.createEmptyMessage() fileMessage.addFileContent(content) fileMessage.send() + messageSentEvent.postValue(Event(fileMessage)) } else { message.addFileContent(content) contentAdded = true @@ -366,6 +372,7 @@ class SendMessageInConversationViewModel if (message.contents.isNotEmpty()) { Log.i("$TAG Sending message") message.send() + messageSentEvent.postValue(Event(message)) } Log.i("$TAG Message sent, re-setting defaults") From 57644a34de18f7a46a378c605e8e055ef5775333 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 8 Jan 2026 11:45:46 +0100 Subject: [PATCH 393/593] Changed mentions color in chat bubble to app's primary --- .../java/org/linphone/ui/main/chat/model/MessageModel.kt | 8 ++++++++ app/src/main/java/org/linphone/utils/AndroidUtils.kt | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt index 8d0ca76e9a..02b9961921 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt @@ -24,6 +24,7 @@ import android.os.CountDownTimer import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned +import android.text.style.ForegroundColorSpan import android.text.style.StyleSpan import androidx.annotation.AnyThread import androidx.annotation.UiThread @@ -744,6 +745,13 @@ class MessageModel start + offset + displayName.length + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE ) + // Change color + spannableBuilder.setSpan( + ForegroundColorSpan(AppUtils.getColorInt(R.color.orange_main_500)), + start + offset, + start + offset + displayName.length + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) offset += displayName.length - source.length } } diff --git a/app/src/main/java/org/linphone/utils/AndroidUtils.kt b/app/src/main/java/org/linphone/utils/AndroidUtils.kt index 1ddf18d13d..e479b53a13 100644 --- a/app/src/main/java/org/linphone/utils/AndroidUtils.kt +++ b/app/src/main/java/org/linphone/utils/AndroidUtils.kt @@ -27,11 +27,13 @@ import android.util.DisplayMetrics import android.util.Rational import android.view.View import androidx.annotation.AnyThread +import androidx.annotation.ColorRes import androidx.annotation.DimenRes import androidx.annotation.MainThread import androidx.annotation.PluralsRes import androidx.annotation.StringRes import androidx.annotation.UiThread +import androidx.core.content.ContextCompat import androidx.core.view.SoftwareKeyboardControllerCompat import java.util.Locale import org.linphone.LinphoneApplication.Companion.coreContext @@ -84,6 +86,11 @@ class AppUtils { return coreContext.context.resources.getQuantityString(id, count, value) } + @AnyThread + fun getColorInt(@ColorRes id: Int): Int { + return ContextCompat.getColor(coreContext.context, id) + } + @MainThread fun getPipRatio( activity: Activity, From 3f868e02fea09845f4cc2693a3edee36a25d432d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 8 Jan 2026 12:06:13 +0100 Subject: [PATCH 394/593] Remove highlight from previous match when navigating between search results --- .../ui/main/chat/model/MessageModel.kt | 30 +++++++++---------- .../chat/viewmodel/ConversationViewModel.kt | 22 +++++++------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt index 02b9961921..a8319a2bf2 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt @@ -676,21 +676,6 @@ class MessageModel return } - // Check for search - if (highlight.isNotEmpty()) { - val indexStart = rawTextContent.indexOf(highlight, 0, ignoreCase = true) - if (indexStart >= 0) { - isTextHighlighted = true - val indexEnd = indexStart + highlight.length - spannableBuilder.setSpan( - StyleSpan(Typeface.BOLD), - indexStart, - indexEnd, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } - } - // Check for mentions val chatRoom = chatMessage.chatRoom val matcher = Pattern.compile(MENTION_REGEXP).matcher(rawTextContent) @@ -787,6 +772,21 @@ class MessageModel ) .build(spannableBuilder) ) + + // Check for search + if (highlight.isNotEmpty()) { + val indexStart = rawTextContent.indexOf(highlight, 0, ignoreCase = true) + if (indexStart >= 0) { + isTextHighlighted = true + val indexEnd = indexStart + highlight.length + spannableBuilder.setSpan( + StyleSpan(Typeface.BOLD), + indexStart, + indexEnd, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } } @WorkerThread diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt index 45c41a9513..616c84937d 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt @@ -152,6 +152,8 @@ class ConversationViewModel private var latestMatch: EventLog? = null + private var latestMatchModel: MessageModel? = null + private val chatRoomListener = object : ChatRoomListenerStub() { @WorkerThread override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) { @@ -381,20 +383,17 @@ class ConversationViewModel @UiThread fun closeSearchBar() { + coreContext.postOnCoreThread { + latestMatchModel?.highlightText("") + latestMatchModel = null + } + searchFilter.value = "" searchBarVisible.value = false focusSearchBarEvent.value = Event(false) latestMatch = null canSearchDown.value = false canSearchUp.value = false - - coreContext.postOnCoreThread { - for (eventLog in eventsList) { - if ((eventLog.model as? MessageModel)?.isTextHighlighted == true) { - eventLog.model.highlightText("") - } - } - } } @UiThread @@ -1031,11 +1030,13 @@ class ConversationViewModel canSearchDown.postValue(true) canSearchUp.postValue(true) + // Clear highlight from previous match + latestMatchModel?.highlightText("") + Log.i( "$TAG Found result [${match.chatMessage?.messageId}] while looking up for message with text [$textToSearch] in direction [$direction] starting from message [${latestMatch?.chatMessage?.messageId}]" ) latestMatch = match - val found = eventsList.find { it.eventLog == match } @@ -1044,7 +1045,8 @@ class ConversationViewModel loadMessagesUpTo(match) } else { Log.i("$TAG Found result is already in history, no need to load more history") - (found.model as? MessageModel)?.highlightText(textToSearch) + latestMatchModel = (found.model as? MessageModel) + latestMatchModel?.highlightText(textToSearch) val index = eventsList.indexOf(found) itemToScrollTo.postValue(index) searchInProgress.postValue(false) From 3b561275a446436688116ff2f025336dc4fbbb3a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 8 Jan 2026 12:17:24 +0100 Subject: [PATCH 395/593] Close search bar when opening bottom sheet and vice versa --- .../linphone/ui/main/chat/fragment/ConversationFragment.kt | 4 ++++ .../main/java/org/linphone/ui/main/chat/model/MessageModel.kt | 1 + 2 files changed, 5 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 1258064420..95abdcdeef 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -790,6 +790,9 @@ open class ConversationFragment : SlidingPaneChildFragment() { viewModel.focusSearchBarEvent.observe(viewLifecycleOwner) { it.consume { show -> if (show) { + val bottomSheetBehavior = BottomSheetBehavior.from(binding.messageBottomSheet.root) + bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + // To automatically open keyboard binding.search.showKeyboard() } else { @@ -1346,6 +1349,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { showDelivery: Boolean = false, showReactions: Boolean = false ) { + viewModel.closeSearchBar() binding.sendArea.messageToSend.hideKeyboard() backPressedCallback.isEnabled = true diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt index a8319a2bf2..cb61500fe2 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt @@ -647,6 +647,7 @@ class MessageModel if (textContent != null) { computeTextContent(textContent, highlight) } + isSelected.postValue(highlight.isNotEmpty()) } @WorkerThread From 07cae7eb125d5e5be08be6f9b14912d2360a5ac8 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 9 Jan 2026 10:30:11 +0100 Subject: [PATCH 396/593] When opening delivery status bottom sheet select 'first' not empty tab automatically --- .../chat/fragment/ConversationFragment.kt | 69 +++++++++++-------- .../main/chat/model/MessageDeliveryModel.kt | 17 +++-- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 95abdcdeef..7b9e53a751 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -96,6 +96,7 @@ import org.linphone.utils.showKeyboard import androidx.core.net.toUri import org.linphone.ui.main.chat.adapter.ConversationParticipantsAdapter import org.linphone.ui.main.chat.model.MessageDeleteDialogModel +import kotlin.collections.arrayListOf @UiThread open class ConversationFragment : SlidingPaneChildFragment() { @@ -1426,49 +1427,59 @@ open class ConversationFragment : SlidingPaneChildFragment() { private fun displayDeliveryStatuses(model: MessageDeliveryModel) { val tabs = binding.messageBottomSheet.tabs tabs.removeAllTabs() - tabs.addTab( - tabs.newTab().setText(model.readLabel.value).setId( - ChatMessage.State.Displayed.toInt() - ) + + val displayedTab = tabs.newTab().setText(model.readLabel.value).setId( + ChatMessage.State.Displayed.toInt() ) - tabs.addTab( - tabs.newTab().setText( - model.receivedLabel.value - ).setId( - ChatMessage.State.DeliveredToUser.toInt() - ) + val deliveredTab = tabs.newTab().setText(model.receivedLabel.value).setId( + ChatMessage.State.DeliveredToUser.toInt() ) - tabs.addTab( - tabs.newTab().setText(model.sentLabel.value).setId( - ChatMessage.State.Delivered.toInt() - ) + val sentTab = tabs.newTab().setText(model.sentLabel.value).setId( + ChatMessage.State.Delivered.toInt() ) - tabs.addTab( - tabs.newTab().setText( - model.errorLabel.value - ).setId( - ChatMessage.State.NotDelivered.toInt() - ) + val errorTab = tabs.newTab().setText(model.errorLabel.value).setId( + ChatMessage.State.NotDelivered.toInt() ) + // Tabs must be added first otherwise select() will do nothing + tabs.addTab(displayedTab) + tabs.addTab(deliveredTab) + tabs.addTab(sentTab) + tabs.addTab(errorTab) + + if (model.displayedModels.isNotEmpty()) { + bottomSheetAdapter.submitList(model.displayedModels) + displayedTab.select() + } else { + if (model.deliveredModels.isNotEmpty()) { + bottomSheetAdapter.submitList(model.deliveredModels) + deliveredTab.select() + } else { + if (model.sentModels.isNotEmpty()) { + bottomSheetAdapter.submitList(model.sentModels) + sentTab.select() + } else { + if (model.errorModels.isNotEmpty()) { + bottomSheetAdapter.submitList(model.errorModels) + errorTab.select() + } else { + // TODO FIXME: remove all tabs and show error message? + } + } + } + } tabs.setOnTabSelectedListener(object : OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab?) { val state = tab?.id ?: ChatMessage.State.Displayed.toInt() bottomSheetAdapter.submitList( - model.computeListForState(ChatMessage.State.fromInt(state)) + model.getListForState(ChatMessage.State.fromInt(state)) ) } - override fun onTabUnselected(tab: TabLayout.Tab?) { - } + override fun onTabUnselected(tab: TabLayout.Tab?) { } - override fun onTabReselected(tab: TabLayout.Tab?) { - } + override fun onTabReselected(tab: TabLayout.Tab?) { } }) - - val initialList = model.displayedModels - bottomSheetAdapter.submitList(initialList) - Log.i("$TAG Submitted [${initialList.size}] items for default delivery status list") } @UiThread diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageDeliveryModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageDeliveryModel.kt index 0721366e9b..3d197e366d 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageDeliveryModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageDeliveryModel.kt @@ -51,11 +51,11 @@ class MessageDeliveryModel val displayedModels = arrayListOf() - private val deliveredModels = arrayListOf() + val deliveredModels = arrayListOf() - private val sentModels = arrayListOf() + val sentModels = arrayListOf() - private val errorModels = arrayListOf() + val errorModels = arrayListOf() private val chatMessageListener = object : ChatMessageListenerStub() { @WorkerThread @@ -63,7 +63,7 @@ class MessageDeliveryModel message: ChatMessage, state: ParticipantImdnState ) { - Log.i("$TAG Participant IMDN state changed [${state.state}], updating delivery status") + Log.i("$TAG Participant IMDN state changed [${state.state}] for message with ID [${message.messageId}], updating delivery status") computeDeliveryStatus() } } @@ -79,7 +79,7 @@ class MessageDeliveryModel } @UiThread - fun computeListForState(state: State): ArrayList { + fun getListForState(state: State): ArrayList { return when (state) { State.DeliveredToUser -> { deliveredModels @@ -98,6 +98,8 @@ class MessageDeliveryModel @WorkerThread private fun computeDeliveryStatus() { + Log.i("$TAG Message ID [${chatMessage.messageId}] is in state [${chatMessage.state}]") + displayedModels.clear() deliveredModels.clear() sentModels.clear() @@ -175,12 +177,15 @@ class MessageDeliveryModel ) ) + if (displayedModels.isEmpty() && deliveredModels.isEmpty() && sentModels.isEmpty() && errorModels.isEmpty()) { + Log.e("$TAG No participant found in state Displayed, DeliveredToUser, Delivered or Error for message ID [${chatMessage.messageId}]") + } + displayedModels.sortBy { it.timestamp } deliveredModels.sortBy { it.timestamp } sentModels.sortBy { it.timestamp } errorModels.sortBy { it.timestamp } - Log.i("$TAG Message ID [${chatMessage.messageId}] is in state [${chatMessage.state}]") Log.i( "$TAG There are [$readCount] that have read this message, [$receivedCount] that have received it, [$sentCount] that haven't received it yet and [$errorCount] that probably won't receive it due to an error" ) From cec3639b737e77f348370d048fcf1eb3e7b8e777 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 9 Jan 2026 10:53:07 +0100 Subject: [PATCH 397/593] Try to workaround race condition on slow phones that may happen with foreground service notification not displayed in time --- .../notifications/NotificationsManager.kt | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index f3291e76b9..03e4d591d6 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -570,25 +570,20 @@ class NotificationsManager Log.i("$TAG Service has been started") inCallService = service + if (waitForInCallServiceForegroundToStopIt) { + Log.w("$TAG Service wasn't started as foreground yet, doing it now using a dummy notification") + showDummyNotificationForCallService() + } + if (inCallServiceForegroundNotificationPublished) { + stopInCallForegroundService() + } + coreContext.postOnCoreThread { core -> - if (core.callsNb == 0) { - Log.w("$TAG No call anymore, stopping service") - if (waitForInCallServiceForegroundToStopIt) { - Log.w("$TAG Service wasn't started as foreground yet, doing it now using a dummy notification") - showDummyNotificationForCallService() - } - if (inCallServiceForegroundNotificationPublished) { - stopInCallForegroundService() - } else { - Log.w("$TAG Foreground service notification wasn't published, shouldn't happen") - } - } else if (currentInCallServiceNotificationId == -1) { + if (core.callsNb >= 1 && currentInCallServiceNotificationId == -1) { val call = core.currentCall ?: core.calls.first() Log.i( "$TAG At least one call is running and no foreground Service notification was found, starting it using call [${call.remoteAddress.asStringUriOnly()}]" ) - - Log.i("$TAG No notification found for this call, creating one now") showCallNotification(call, LinphoneUtils.isCallIncoming(call.state)) } } From a816a956b8be5e81dcb6a1f192fcdaa81185ffff Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 12 Jan 2026 15:12:14 +0100 Subject: [PATCH 398/593] Added missing hover effects --- app/src/main/res/color/danger_500.xml | 2 + .../main/res/color/file_viewer_main2_500.xml | 6 ++ .../res/drawable/shape_circle_background.xml | 9 ++ ...pe_dark_red_outlined_button_background.xml | 6 ++ .../shape_red_outlined_button_background.xml | 2 +- .../shape_squircle_light_blue_background.xml | 5 ++ .../shape_squircle_transparent_background.xml | 5 ++ ...shape_squircle_white_border_background.xml | 5 ++ ...ircle_emoji_reaction_button_background.xml | 11 +++ ...quircle_red_outlined_button_background.xml | 9 ++ ...squircle_transparent_button_background.xml | 9 ++ ...uircle_transparent_button_background_2.xml | 9 ++ .../res/drawable/transparent_background.xml | 9 ++ .../main/res/layout-land/bottom_nav_bar.xml | 12 +++ .../contact_new_or_edit_fragment.xml | 6 ++ .../dialog_zrtp_sas_validation.xml | 3 +- .../dialog_zrtp_security_alert.xml | 2 +- .../res/layout-land/main_activity_top_bar.xml | 18 ++-- .../meeting_waiting_room_fragment.xml | 5 +- .../layout-sw600dp-land/bottom_nav_bar.xml | 12 +++ .../main_activity_top_bar.xml | 18 ++-- .../assistant_landing_fragment.xml | 7 +- ...third_party_sip_account_login_fragment.xml | 7 +- .../res/layout/account_profile_fragment.xml | 28 +++--- .../res/layout/account_settings_fragment.xml | 18 ++-- .../res/layout/address_selected_list_cell.xml | 2 +- .../res/layout/assistant_landing_fragment.xml | 5 +- .../assistant_recover_account_fragment.xml | 1 + ...third_party_sip_account_login_fragment.xml | 9 +- app/src/main/res/layout/bottom_nav_bar.xml | 20 +++-- .../res/layout/chat_conversation_fragment.xml | 16 ++-- .../res/layout/chat_emoji_reaction_picker.xml | 16 ++-- .../chat_ephemeral_lifetime_fragment.xml | 30 ++----- .../main/res/layout/chat_info_fragment.xml | 25 +++--- app/src/main/res/layout/contact_fragment.xml | 23 ++--- .../res/layout/contact_new_or_edit_cell.xml | 6 +- .../layout/contact_new_or_edit_fragment.xml | 6 ++ .../res/layout/contacts_list_fragment.xml | 9 +- .../res/layout/dialog_zrtp_sas_validation.xml | 5 +- .../res/layout/dialog_zrtp_security_alert.xml | 2 +- app/src/main/res/layout/drawer_menu.xml | 13 ++- .../res/layout/drawer_shortcuts_list_cell.xml | 3 +- .../res/layout/file_media_viewer_activity.xml | 21 +++-- .../main/res/layout/file_viewer_activity.xml | 21 +++-- .../main/res/layout/main_activity_top_bar.xml | 18 ++-- .../layout/meeting_waiting_room_fragment.xml | 6 +- .../res/layout/recording_player_fragment.xml | 21 +++-- .../settings_advanced_calls_fragment.xml | 36 ++++---- .../res/layout/settings_advanced_fragment.xml | 10 ++- app/src/main/res/layout/settings_calls.xml | 2 + app/src/main/res/layout/settings_contacts.xml | 2 + ...ttings_contacts_carddav_ldap_list_cell.xml | 1 + app/src/main/res/layout/settings_fragment.xml | 90 ++++++++++--------- .../main/res/layout/start_call_fragment.xml | 15 ++-- .../layout/start_call_numpad_bottom_sheet.xml | 3 +- .../main/res/layout/start_chat_fragment.xml | 15 ++-- app/src/main/res/values-night/themes.xml | 2 +- app/src/main/res/values/dimen.xml | 1 + 58 files changed, 433 insertions(+), 245 deletions(-) create mode 100644 app/src/main/res/color/file_viewer_main2_500.xml create mode 100644 app/src/main/res/drawable/shape_circle_background.xml create mode 100644 app/src/main/res/drawable/shape_dark_red_outlined_button_background.xml create mode 100644 app/src/main/res/drawable/shape_squircle_light_blue_background.xml create mode 100644 app/src/main/res/drawable/shape_squircle_transparent_background.xml create mode 100644 app/src/main/res/drawable/shape_squircle_white_border_background.xml create mode 100644 app/src/main/res/drawable/squircle_emoji_reaction_button_background.xml create mode 100644 app/src/main/res/drawable/squircle_red_outlined_button_background.xml create mode 100644 app/src/main/res/drawable/squircle_transparent_button_background.xml create mode 100644 app/src/main/res/drawable/squircle_transparent_button_background_2.xml create mode 100644 app/src/main/res/drawable/transparent_background.xml diff --git a/app/src/main/res/color/danger_500.xml b/app/src/main/res/color/danger_500.xml index bb77921219..ea14a77612 100644 --- a/app/src/main/res/color/danger_500.xml +++ b/app/src/main/res/color/danger_500.xml @@ -1,4 +1,6 @@ + + diff --git a/app/src/main/res/color/file_viewer_main2_500.xml b/app/src/main/res/color/file_viewer_main2_500.xml new file mode 100644 index 0000000000..83e5a29594 --- /dev/null +++ b/app/src/main/res/color/file_viewer_main2_500.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/shape_circle_background.xml b/app/src/main/res/drawable/shape_circle_background.xml new file mode 100644 index 0000000000..2dddedb2ed --- /dev/null +++ b/app/src/main/res/drawable/shape_circle_background.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_dark_red_outlined_button_background.xml b/app/src/main/res/drawable/shape_dark_red_outlined_button_background.xml new file mode 100644 index 0000000000..1082d564da --- /dev/null +++ b/app/src/main/res/drawable/shape_dark_red_outlined_button_background.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_red_outlined_button_background.xml b/app/src/main/res/drawable/shape_red_outlined_button_background.xml index a7cb8b9784..a3b7d55c9b 100644 --- a/app/src/main/res/drawable/shape_red_outlined_button_background.xml +++ b/app/src/main/res/drawable/shape_red_outlined_button_background.xml @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_squircle_light_blue_background.xml b/app/src/main/res/drawable/shape_squircle_light_blue_background.xml new file mode 100644 index 0000000000..74239dad33 --- /dev/null +++ b/app/src/main/res/drawable/shape_squircle_light_blue_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_squircle_transparent_background.xml b/app/src/main/res/drawable/shape_squircle_transparent_background.xml new file mode 100644 index 0000000000..2f69733cb8 --- /dev/null +++ b/app/src/main/res/drawable/shape_squircle_transparent_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_squircle_white_border_background.xml b/app/src/main/res/drawable/shape_squircle_white_border_background.xml new file mode 100644 index 0000000000..8584f68279 --- /dev/null +++ b/app/src/main/res/drawable/shape_squircle_white_border_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/squircle_emoji_reaction_button_background.xml b/app/src/main/res/drawable/squircle_emoji_reaction_button_background.xml new file mode 100644 index 0000000000..33400bcc18 --- /dev/null +++ b/app/src/main/res/drawable/squircle_emoji_reaction_button_background.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/squircle_red_outlined_button_background.xml b/app/src/main/res/drawable/squircle_red_outlined_button_background.xml new file mode 100644 index 0000000000..622ab215f2 --- /dev/null +++ b/app/src/main/res/drawable/squircle_red_outlined_button_background.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/squircle_transparent_button_background.xml b/app/src/main/res/drawable/squircle_transparent_button_background.xml new file mode 100644 index 0000000000..d089794b3e --- /dev/null +++ b/app/src/main/res/drawable/squircle_transparent_button_background.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/squircle_transparent_button_background_2.xml b/app/src/main/res/drawable/squircle_transparent_button_background_2.xml new file mode 100644 index 0000000000..d21a76e33c --- /dev/null +++ b/app/src/main/res/drawable/squircle_transparent_button_background_2.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/transparent_background.xml b/app/src/main/res/drawable/transparent_background.xml new file mode 100644 index 0000000000..c6c6f2da1a --- /dev/null +++ b/app/src/main/res/drawable/transparent_background.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/bottom_nav_bar.xml b/app/src/main/res/layout-land/bottom_nav_bar.xml index dad7c51d4c..4c0a561013 100644 --- a/app/src/main/res/layout-land/bottom_nav_bar.xml +++ b/app/src/main/res/layout-land/bottom_nav_bar.xml @@ -24,9 +24,12 @@ android:onClick="@{() -> viewModel.navigateToContacts()}" android:layout_width="0dp" android:layout_height="wrap_content" + android:paddingTop="5dp" + android:paddingBottom="5dp" android:drawableTop="@drawable/address_book" android:drawablePadding="10dp" android:drawableTint="@{viewModel.contactsSelected ? @color/main1_500 : @color/main2_600, default=@color/main2_600}" + android:background="@drawable/squircle_transparent_button_background" android:text="@string/bottom_navigation_contacts_label" textFont="@{viewModel.contactsSelected ? NotoSansFont.NotoSansBold : NotoSansFont.NotoSansRegular}" app:layout_constraintBottom_toTopOf="@id/calls" @@ -40,9 +43,12 @@ android:onClick="@{() -> viewModel.navigateToHistory()}" android:layout_width="0dp" android:layout_height="wrap_content" + android:paddingTop="5dp" + android:paddingBottom="5dp" android:drawableTop="@drawable/phone" android:drawableTint="@{viewModel.callsSelected ? @color/main1_500 : @color/main2_600, default=@color/main2_600}" android:drawablePadding="10dp" + android:background="@drawable/squircle_transparent_button_background" android:text="@string/bottom_navigation_calls_label" textFont="@{viewModel.callsSelected ? NotoSansFont.NotoSansBold : NotoSansFont.NotoSansRegular}" app:layout_constraintBottom_toTopOf="@id/conversations" @@ -68,9 +74,12 @@ android:onClick="@{() -> viewModel.navigateToConversations()}" android:layout_width="0dp" android:layout_height="wrap_content" + android:paddingTop="5dp" + android:paddingBottom="5dp" android:drawableTop="@drawable/chat_teardrop_text" android:drawablePadding="10dp" android:drawableTint="@{viewModel.conversationsSelected ? @color/main1_500 : @color/main2_600, default=@color/main2_600}" + android:background="@drawable/squircle_transparent_button_background" android:text="@string/bottom_navigation_conversations_label" android:visibility="@{viewModel.hideConversations ? View.GONE : View.VISIBLE}" textFont="@{viewModel.conversationsSelected ? NotoSansFont.NotoSansBold : NotoSansFont.NotoSansRegular}" @@ -97,9 +106,12 @@ android:onClick="@{() -> viewModel.navigateToMeetings()}" android:layout_width="0dp" android:layout_height="wrap_content" + android:paddingTop="5dp" + android:paddingBottom="5dp" android:drawableTop="@drawable/video_conference" android:drawablePadding="10dp" android:drawableTint="@{viewModel.meetingsSelected ? @color/main1_500 : @color/main2_600, default=@color/main2_600}" + android:background="@drawable/squircle_transparent_button_background" android:text="@string/bottom_navigation_meetings_label" android:visibility="@{viewModel.hideMeetings ? View.GONE : View.VISIBLE}" textFont="@{viewModel.meetingsSelected ? NotoSansFont.NotoSansBold : NotoSansFont.NotoSansRegular}" diff --git a/app/src/main/res/layout-land/contact_new_or_edit_fragment.xml b/app/src/main/res/layout-land/contact_new_or_edit_fragment.xml index d86f806823..b22080b55f 100644 --- a/app/src/main/res/layout-land/contact_new_or_edit_fragment.xml +++ b/app/src/main/res/layout-land/contact_new_or_edit_fragment.xml @@ -99,10 +99,12 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" + android:padding="5dp" android:text="@string/manage_account_add_picture" android:textSize="14sp" android:drawableStart="@drawable/camera" android:drawablePadding="3dp" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.picturePath.empty ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintTop_toBottomOf="@id/avatar" app:layout_constraintStart_toStartOf="parent" @@ -115,10 +117,12 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" + android:padding="5dp" android:text="@string/manage_account_edit_picture" android:textSize="14sp" android:drawableStart="@drawable/pencil_simple" android:drawablePadding="3dp" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.picturePath.empty ? View.GONE : View.VISIBLE}" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintTop_toBottomOf="@id/avatar" @@ -133,10 +137,12 @@ android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginStart="16dp" + android:padding="5dp" android:text="@string/manage_account_remove_picture" android:textSize="14sp" android:drawableStart="@drawable/trash_simple" android:drawablePadding="3dp" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.picturePath.empty ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toBottomOf="@id/avatar" app:layout_constraintStart_toEndOf="@id/edit_picture_label" diff --git a/app/src/main/res/layout-land/dialog_zrtp_sas_validation.xml b/app/src/main/res/layout-land/dialog_zrtp_sas_validation.xml index 2d4c11e8e4..6cfb3a27f5 100644 --- a/app/src/main/res/layout-land/dialog_zrtp_sas_validation.xml +++ b/app/src/main/res/layout-land/dialog_zrtp_sas_validation.xml @@ -66,6 +66,7 @@ android:text="@string/call_zrtp_sas_validation_skip" android:textSize="13sp" android:textColor="@color/bc_white" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"/> @@ -246,7 +247,7 @@ android:paddingBottom="@dimen/primary_secondary_buttons_label_padding" android:paddingTop="@dimen/primary_secondary_buttons_label_padding" android:gravity="center" - android:background="@drawable/shape_red_outlined_button_background" + android:background="@drawable/squircle_red_outlined_button_background" android:text="@string/call_dialog_zrtp_validate_trust_letters_do_not_match" android:textSize="13sp" android:textColor="?attr/color_danger_500" diff --git a/app/src/main/res/layout-land/dialog_zrtp_security_alert.xml b/app/src/main/res/layout-land/dialog_zrtp_security_alert.xml index 76a072f1df..9caca0a26e 100644 --- a/app/src/main/res/layout-land/dialog_zrtp_security_alert.xml +++ b/app/src/main/res/layout-land/dialog_zrtp_security_alert.xml @@ -122,7 +122,7 @@ android:paddingBottom="@dimen/primary_secondary_buttons_label_padding" android:paddingTop="@dimen/primary_secondary_buttons_label_padding" android:gravity="center" - android:background="@drawable/shape_red_button_background" + android:background="@drawable/squircle_red_button_background" android:text="@string/call_action_hang_up" android:textSize="13sp" android:textColor="@color/bc_white" diff --git a/app/src/main/res/layout-land/main_activity_top_bar.xml b/app/src/main/res/layout-land/main_activity_top_bar.xml index 87418db2d7..cc52335a72 100644 --- a/app/src/main/res/layout-land/main_activity_top_bar.xml +++ b/app/src/main/res/layout-land/main_activity_top_bar.xml @@ -54,10 +54,9 @@ android:layout_height="0dp" android:src="@drawable/list" android:contentDescription="@string/content_description_open_filter" - app:layout_constraintDimensionRatio="1:1" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@id/avatar" - app:layout_constraintBottom_toBottomOf="@id/avatar" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" app:tint="?attr/color_on_main" /> @@ -252,6 +254,7 @@ android:text="@string/assistant_forgotten_password" android:textSize="13sp" android:textColor="?attr/color_main2_500" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintStart_toEndOf="@id/login" app:layout_constraintTop_toTopOf="@id/login" app:layout_constraintBottom_toBottomOf="@id/login"/> diff --git a/app/src/main/res/layout-sw600dp/assistant_third_party_sip_account_login_fragment.xml b/app/src/main/res/layout-sw600dp/assistant_third_party_sip_account_login_fragment.xml index f8b472c210..96d1bdda14 100644 --- a/app/src/main/res/layout-sw600dp/assistant_third_party_sip_account_login_fragment.xml +++ b/app/src/main/res/layout-sw600dp/assistant_third_party_sip_account_login_fragment.xml @@ -299,13 +299,14 @@ android:id="@+id/advanced_settings" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="32dp" - android:layout_marginTop="35dp" - android:padding="5dp" + android:layout_marginStart="20dp" + android:layout_marginTop="25dp" + android:padding="10dp" android:text="@string/settings_advanced_title" android:drawableEnd="@{viewModel.expandAdvancedSettings ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" android:drawablePadding="10dp" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintWidth_max="@dimen/button_max_width" app:layout_constraintStart_toEndOf="@id/title" app:layout_constraintEnd_toStartOf="@id/logo" diff --git a/app/src/main/res/layout/account_profile_fragment.xml b/app/src/main/res/layout/account_profile_fragment.xml index 96b0944ecf..e6f38891ad 100644 --- a/app/src/main/res/layout/account_profile_fragment.xml +++ b/app/src/main/res/layout/account_profile_fragment.xml @@ -118,10 +118,12 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" + android:padding="5dp" android:text="@string/manage_account_add_picture" android:textSize="14sp" android:drawableStart="@drawable/camera" android:drawablePadding="3dp" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.accountModel.picturePath.length() == 0 ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintTop_toBottomOf="@id/avatar" app:layout_constraintStart_toStartOf="parent" @@ -134,11 +136,13 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" + android:padding="5dp" android:text="@string/manage_account_edit_picture" android:textSize="14sp" android:drawableStart="@drawable/pencil_simple" android:drawablePadding="3dp" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.accountModel.picturePath.length() == 0 ? View.GONE : View.VISIBLE}" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintTop_toBottomOf="@id/avatar" @@ -153,11 +157,13 @@ android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginStart="16dp" + android:padding="5dp" android:text="@string/manage_account_remove_picture" android:textSize="14sp" android:drawableStart="@drawable/trash_simple" android:drawablePadding="3dp" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.accountModel.picturePath.length() == 0 ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toBottomOf="@id/avatar" app:layout_constraintStart_toEndOf="@id/edit_picture_label" @@ -176,13 +182,14 @@ android:id="@+id/details" android:layout_width="0dp" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" - android:layout_marginTop="32dp" + android:padding="10dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="20dp" android:text="@string/manage_account_details_title" android:drawableEnd="@{viewModel.expandDetails ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/avatar_barrier"/> @@ -422,14 +429,15 @@ android:id="@+id/devices" android:layout_width="0dp" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" - android:layout_marginTop="32dp" + android:padding="10dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="20dp" android:text="@string/manage_account_devices_title" android:visibility="@{viewModel.isOnDefaultDomain ? View.VISIBLE : View.GONE, default=gone}" android:drawableEnd="@{viewModel.expandDevices ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/connection_background"/> @@ -490,8 +498,8 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" + android:layout_marginStart="20dp" + android:layout_marginEnd="20dp" android:layout_marginTop="16dp" android:text="@string/contact_details_actions_title" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/account_settings_fragment.xml b/app/src/main/res/layout/account_settings_fragment.xml index f664432aec..84b55e7a6a 100644 --- a/app/src/main/res/layout/account_settings_fragment.xml +++ b/app/src/main/res/layout/account_settings_fragment.xml @@ -268,14 +268,15 @@ android:onClick="@{() -> viewModel.toggleNatPolicySettingsExpand()}" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" + android:padding="10dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" android:layout_marginTop="20dp" android:layout_marginBottom="10dp" android:text="@string/account_settings_nat_policy_title" android:drawableEnd="@{viewModel.expandNatPolicySettings ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" - android:drawableTint="?attr/color_main2_600"/> + android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" /> + android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" /> diff --git a/app/src/main/res/layout/assistant_recover_account_fragment.xml b/app/src/main/res/layout/assistant_recover_account_fragment.xml index 33531c8cb6..f11d3fac26 100644 --- a/app/src/main/res/layout/assistant_recover_account_fragment.xml +++ b/app/src/main/res/layout/assistant_recover_account_fragment.xml @@ -35,6 +35,7 @@ android:layout_height="wrap_content"> diff --git a/app/src/main/res/layout/bottom_nav_bar.xml b/app/src/main/res/layout/bottom_nav_bar.xml index d8f82ffee3..4074ef9165 100644 --- a/app/src/main/res/layout/bottom_nav_bar.xml +++ b/app/src/main/res/layout/bottom_nav_bar.xml @@ -24,9 +24,10 @@ android:onClick="@{() -> viewModel.navigateToContacts()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="12dp" - android:layout_marginBottom="12dp" + android:paddingTop="12dp" + android:paddingBottom="12dp" android:drawableTop="@drawable/address_book" + android:background="@drawable/squircle_transparent_button_background" android:drawablePadding="4dp" android:drawableTint="@{viewModel.contactsSelected ? @color/main1_500 : @color/main2_600, default=@color/main2_600}" android:text="@string/bottom_navigation_contacts_label" @@ -42,9 +43,10 @@ android:onClick="@{() -> viewModel.navigateToHistory()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="12dp" - android:layout_marginBottom="12dp" + android:paddingTop="12dp" + android:paddingBottom="12dp" android:drawableTop="@drawable/phone" + android:background="@drawable/squircle_transparent_button_background" android:drawablePadding="4dp" android:drawableTint="@{viewModel.callsSelected ? @color/main1_500 : @color/main2_600, default=@color/main2_600}" android:text="@string/bottom_navigation_calls_label" @@ -73,9 +75,10 @@ android:onClick="@{() -> viewModel.navigateToConversations()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="12dp" - android:layout_marginBottom="12dp" + android:paddingTop="12dp" + android:paddingBottom="12dp" android:drawableTop="@drawable/chat_teardrop_text" + android:background="@drawable/squircle_transparent_button_background" android:drawablePadding="4dp" android:drawableTint="@{viewModel.conversationsSelected ? @color/main1_500 : @color/main2_600, default=@color/main2_600}" android:text="@string/bottom_navigation_conversations_label" @@ -105,9 +108,10 @@ android:onClick="@{() -> viewModel.navigateToMeetings()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="12dp" - android:layout_marginBottom="12dp" + android:paddingTop="12dp" + android:paddingBottom="12dp" android:drawableTop="@drawable/video_conference" + android:background="@drawable/squircle_transparent_button_background" android:drawablePadding="4dp" android:drawableTint="@{viewModel.meetingsSelected ? @color/main1_500 : @color/main2_600, default=@color/main2_600}" android:text="@string/bottom_navigation_meetings_label" diff --git a/app/src/main/res/layout/chat_conversation_fragment.xml b/app/src/main/res/layout/chat_conversation_fragment.xml index 8a58a9c2d7..75e8a1f5d7 100644 --- a/app/src/main/res/layout/chat_conversation_fragment.xml +++ b/app/src/main/res/layout/chat_conversation_fragment.xml @@ -73,14 +73,15 @@ style="@style/icon_top_bar_button_style" android:id="@+id/back" android:layout_width="wrap_content" - android:layout_height="@dimen/top_bar_height" + android:layout_height="0dp" android:onClick="@{backClickListener}" android:visibility="@{viewModel.isCallConversation || viewModel.showBackButton && !viewModel.searchBarVisible ? View.VISIBLE : View.GONE}" android:src="@drawable/caret_left" android:contentDescription="@string/content_description_go_back_icon" app:tint="?attr/color_main1_500" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"/> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@id/events_list"/> + app:layout_constraintTop_toTopOf="@id/events_list" /> diff --git a/app/src/main/res/layout/chat_emoji_reaction_picker.xml b/app/src/main/res/layout/chat_emoji_reaction_picker.xml index e972b58106..d82e3cb584 100644 --- a/app/src/main/res/layout/chat_emoji_reaction_picker.xml +++ b/app/src/main/res/layout/chat_emoji_reaction_picker.xml @@ -40,7 +40,8 @@ android:paddingBottom="3dp" android:text="@string/emoji_thumbs_up" android:textSize="@dimen/chat_bubble_long_press_emoji_reaction_size" - android:background="@{model.ourReactionIndex == 0 ? @drawable/shape_squircle_gray_200_background : @drawable/shape_empty, default=@drawable/shape_squircle_gray_200_background}" + android:selected="@{model.ourReactionIndex == 0}" + android:background="@drawable/squircle_emoji_reaction_button_background" app:layout_constraintHorizontal_chainStyle="spread" app:layout_constraintTop_toTopOf="@id/emojis_background" app:layout_constraintBottom_toBottomOf="@id/emojis_background" @@ -59,7 +60,8 @@ android:paddingBottom="3dp" android:text="@string/emoji_love" android:textSize="@dimen/chat_bubble_long_press_emoji_reaction_size" - android:background="@{model.ourReactionIndex == 1 ? @drawable/shape_squircle_gray_200_background : @drawable/shape_empty}" + android:selected="@{model.ourReactionIndex == 1}" + android:background="@drawable/squircle_emoji_reaction_button_background" app:layout_constraintTop_toTopOf="@id/thumbs_up" app:layout_constraintBottom_toBottomOf="@id/thumbs_up" app:layout_constraintStart_toEndOf="@id/thumbs_up" @@ -77,7 +79,8 @@ android:paddingBottom="3dp" android:text="@string/emoji_laughing" android:textSize="@dimen/chat_bubble_long_press_emoji_reaction_size" - android:background="@{model.ourReactionIndex == 2 ? @drawable/shape_squircle_gray_200_background : @drawable/shape_empty}" + android:selected="@{model.ourReactionIndex == 2}" + android:background="@drawable/squircle_emoji_reaction_button_background" app:layout_constraintTop_toTopOf="@id/thumbs_up" app:layout_constraintBottom_toBottomOf="@id/thumbs_up" app:layout_constraintStart_toEndOf="@id/love" @@ -95,7 +98,8 @@ android:paddingBottom="3dp" android:text="@string/emoji_surprised" android:textSize="@dimen/chat_bubble_long_press_emoji_reaction_size" - android:background="@{model.ourReactionIndex == 3 ? @drawable/shape_squircle_gray_200_background : @drawable/shape_empty}" + android:selected="@{model.ourReactionIndex == 3}" + android:background="@drawable/squircle_emoji_reaction_button_background" app:layout_constraintTop_toTopOf="@id/thumbs_up" app:layout_constraintBottom_toBottomOf="@id/thumbs_up" app:layout_constraintStart_toEndOf="@id/laughing" @@ -113,7 +117,8 @@ android:paddingBottom="3dp" android:text="@string/emoji_tear" android:textSize="@dimen/chat_bubble_long_press_emoji_reaction_size" - android:background="@{model.ourReactionIndex == 4 ? @drawable/shape_squircle_gray_200_background : @drawable/shape_empty}" + android:selected="@{model.ourReactionIndex == 4}" + android:background="@drawable/squircle_emoji_reaction_button_background" app:layout_constraintTop_toTopOf="@id/thumbs_up" app:layout_constraintBottom_toBottomOf="@id/thumbs_up" app:layout_constraintStart_toEndOf="@id/surprised" @@ -131,6 +136,7 @@ android:paddingTop="10dp" android:adjustViewBounds="true" android:src="@drawable/plus_circle" + android:background="@drawable/squircle_emoji_reaction_button_background" android:contentDescription="@string/content_description_chat_open_emoji_picker" app:layout_constraintStart_toEndOf="@id/tear" app:layout_constraintEnd_toEndOf="@id/emojis_background" diff --git a/app/src/main/res/layout/chat_ephemeral_lifetime_fragment.xml b/app/src/main/res/layout/chat_ephemeral_lifetime_fragment.xml index cf352c09ea..21297c9b95 100644 --- a/app/src/main/res/layout/chat_ephemeral_lifetime_fragment.xml +++ b/app/src/main/res/layout/chat_ephemeral_lifetime_fragment.xml @@ -89,6 +89,8 @@ android:layout_marginTop="25dp" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" + android:paddingStart="16dp" + android:paddingEnd="16dp" android:background="@drawable/shape_squircle_white_background" app:layout_constraintTop_toBottomOf="@id/subtitle"> @@ -97,8 +99,6 @@ android:onClick="@{() -> viewModel.onValueSelected(60)}" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginEnd="16dp" android:paddingTop="10dp" android:paddingBottom="10dp" android:checked="@{viewModel.currentlySelectedValue == 60}" @@ -106,14 +106,13 @@ android:textSize="17sp" android:textColor="?attr/color_main2_500" android:textAlignment="textStart" + android:background="@drawable/action_background_middle" app:useMaterialThemeColors="false" app:buttonTint="?attr/color_main1_500" /> @@ -122,8 +121,6 @@ android:onClick="@{() -> viewModel.onValueSelected(3600)}" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginEnd="16dp" android:paddingTop="10dp" android:paddingBottom="10dp" android:checked="@{viewModel.currentlySelectedValue == 3600}" @@ -131,14 +128,13 @@ android:textSize="17sp" android:textColor="?attr/color_main2_500" android:textAlignment="textStart" + android:background="@drawable/action_background_middle" app:useMaterialThemeColors="false" app:buttonTint="?attr/color_main1_500" /> @@ -147,8 +143,6 @@ android:onClick="@{() -> viewModel.onValueSelected(86400)}" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginEnd="16dp" android:paddingTop="10dp" android:paddingBottom="10dp" android:checked="@{viewModel.currentlySelectedValue == 86400}" @@ -156,14 +150,13 @@ android:textSize="17sp" android:textColor="?attr/color_main2_500" android:textAlignment="textStart" + android:background="@drawable/action_background_middle" app:useMaterialThemeColors="false" app:buttonTint="?attr/color_main1_500" /> @@ -172,8 +165,6 @@ android:onClick="@{() -> viewModel.onValueSelected(259200)}" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginEnd="16dp" android:paddingTop="10dp" android:paddingBottom="10dp" android:checked="@{viewModel.currentlySelectedValue == 259200}" @@ -181,14 +172,13 @@ android:textSize="17sp" android:textColor="?attr/color_main2_500" android:textAlignment="textStart" + android:background="@drawable/action_background_middle" app:useMaterialThemeColors="false" app:buttonTint="?attr/color_main1_500" /> @@ -197,8 +187,6 @@ android:onClick="@{() -> viewModel.onValueSelected(604800)}" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginEnd="16dp" android:paddingTop="10dp" android:paddingBottom="10dp" android:checked="@{viewModel.currentlySelectedValue == 604800}" @@ -206,14 +194,13 @@ android:textSize="17sp" android:textColor="?attr/color_main2_500" android:textAlignment="textStart" + android:background="@drawable/action_background_middle" app:useMaterialThemeColors="false" app:buttonTint="?attr/color_main1_500" /> @@ -222,8 +209,6 @@ android:onClick="@{() -> viewModel.onValueSelected(0)}" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginEnd="16dp" android:paddingTop="10dp" android:paddingBottom="10dp" android:checked="@{viewModel.currentlySelectedValue == 0}" @@ -231,6 +216,7 @@ android:textSize="17sp" android:textColor="?attr/color_main2_500" android:textAlignment="textStart" + android:background="@drawable/action_background_middle" app:useMaterialThemeColors="false" app:buttonTint="?attr/color_main1_500" /> diff --git a/app/src/main/res/layout/chat_info_fragment.xml b/app/src/main/res/layout/chat_info_fragment.xml index 40c35a5192..32d9e452b5 100644 --- a/app/src/main/res/layout/chat_info_fragment.xml +++ b/app/src/main/res/layout/chat_info_fragment.xml @@ -127,11 +127,13 @@ @@ -374,9 +377,9 @@ style="@style/section_header_style" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="26dp" + android:layout_marginStart="20dp" android:layout_marginTop="16dp" - android:layout_marginEnd="26dp" + android:layout_marginEnd="20dp" android:padding="5dp" android:text="@string/conversation_details_media_documents_title" app:layout_constraintBottom_toTopOf="@id/action_media" @@ -432,9 +435,9 @@ style="@style/section_header_style" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="26dp" + android:layout_marginStart="20dp" android:layout_marginTop="16dp" - android:layout_marginEnd="26dp" + android:layout_marginEnd="20dp" android:padding="5dp" android:text="@string/contact_details_actions_title" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/contact_fragment.xml b/app/src/main/res/layout/contact_fragment.xml index 3915c4d147..19a263c987 100644 --- a/app/src/main/res/layout/contact_fragment.xml +++ b/app/src/main/res/layout/contact_fragment.xml @@ -244,13 +244,14 @@ android:onClick="@{() -> viewModel.toggleNumbersAndAddressesExpand()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" - android:layout_marginTop="32dp" + android:padding="10dp" + android:layout_marginStart="20dp" + android:layout_marginEnd="20dp" + android:layout_marginTop="20dp" android:text="@string/contact_details_numbers_and_addresses_title" android:drawableEnd="@{viewModel.expandNumbersAndAddresses ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.atLeastOneSipAddressOrPhoneNumber ? View.VISIBLE : View.GONE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -350,13 +351,14 @@ android:onClick="@{() -> viewModel.displayTrustDialog()}" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="26dp" - android:layout_marginTop="16dp" - android:padding="5dp" + android:layout_marginStart="20dp" + android:layout_marginTop="10dp" + android:padding="10dp" android:text="@string/contact_details_trust_title" android:drawableEnd="@drawable/question" android:drawableTint="?attr/color_main2_600" android:drawablePadding="8dp" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.showContactTrustAndDevices ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/info_background"/> @@ -367,11 +369,12 @@ android:onClick="@{() -> viewModel.toggleDevicesTrustExpand()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginEnd="26dp" - android:layout_marginTop="16dp" - android:padding="5dp" + android:layout_marginEnd="20dp" + android:layout_marginTop="10dp" + android:padding="10dp" android:drawableEnd="@{viewModel.expandDevicesTrust ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.showContactTrustAndDevices ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintStart_toEndOf="@id/trust_label" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/contact_new_or_edit_cell.xml b/app/src/main/res/layout/contact_new_or_edit_cell.xml index d7dab0d485..e63d7874c5 100644 --- a/app/src/main/res/layout/contact_new_or_edit_cell.xml +++ b/app/src/main/res/layout/contact_new_or_edit_cell.xml @@ -40,10 +40,12 @@ android:onClick="@{() -> model.remove()}" android:id="@+id/remove" android:visibility="@{model.showRemoveButton ? View.VISIBLE : View.INVISIBLE, default=invisible}" - android:layout_width="@dimen/icon_size" - android:layout_height="@dimen/icon_size" + android:layout_width="@dimen/large_icon_size" + android:layout_height="@dimen/large_icon_size" + android:padding="5dp" android:layout_marginStart="10dp" android:src="@drawable/x" + android:background="@drawable/squircle_transparent_button_background" android:contentDescription="@string/content_description_contact_remove_field" app:tint="?attr/color_main2_700" app:layout_constraintStart_toEndOf="@id/field" diff --git a/app/src/main/res/layout/contact_new_or_edit_fragment.xml b/app/src/main/res/layout/contact_new_or_edit_fragment.xml index d0580d955c..a00ead9d57 100644 --- a/app/src/main/res/layout/contact_new_or_edit_fragment.xml +++ b/app/src/main/res/layout/contact_new_or_edit_fragment.xml @@ -98,10 +98,12 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" + android:padding="5dp" android:text="@string/manage_account_add_picture" android:textSize="14sp" android:drawableStart="@drawable/camera" android:drawablePadding="3dp" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.picturePath.empty ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintTop_toBottomOf="@id/avatar" app:layout_constraintStart_toStartOf="parent" @@ -114,10 +116,12 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" + android:padding="5dp" android:text="@string/manage_account_edit_picture" android:textSize="14sp" android:drawableStart="@drawable/pencil_simple" android:drawablePadding="3dp" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.picturePath.empty ? View.GONE : View.VISIBLE}" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintTop_toBottomOf="@id/avatar" @@ -132,10 +136,12 @@ android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginStart="16dp" + android:padding="5dp" android:text="@string/manage_account_remove_picture" android:textSize="14sp" android:drawableStart="@drawable/trash_simple" android:drawablePadding="3dp" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.picturePath.empty ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toBottomOf="@id/avatar" app:layout_constraintStart_toEndOf="@id/edit_picture_label" diff --git a/app/src/main/res/layout/contacts_list_fragment.xml b/app/src/main/res/layout/contacts_list_fragment.xml index 707f91f95e..3c31fa0108 100644 --- a/app/src/main/res/layout/contacts_list_fragment.xml +++ b/app/src/main/res/layout/contacts_list_fragment.xml @@ -77,12 +77,13 @@ onClickListener="@{() -> viewModel.toggleFavouritesVisibility()}" android:layout_width="match_parent" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="16dp" - android:layout_marginEnd="16dp" - android:layout_marginTop="10dp" + android:padding="10dp" + android:layout_marginStart="10dp" + android:layout_marginEnd="10dp" + android:layout_marginTop="5dp" android:layout_marginBottom="4dp" android:text="@string/contacts_list_favourites_title" + android:background="@drawable/squircle_transparent_button_background" android:drawableEnd="@{viewModel.showFavourites ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" /> diff --git a/app/src/main/res/layout/dialog_zrtp_sas_validation.xml b/app/src/main/res/layout/dialog_zrtp_sas_validation.xml index b2c4fb8a3a..ea0ecf693f 100644 --- a/app/src/main/res/layout/dialog_zrtp_sas_validation.xml +++ b/app/src/main/res/layout/dialog_zrtp_sas_validation.xml @@ -63,6 +63,7 @@ android:text="@string/call_zrtp_sas_validation_skip" android:textSize="13sp" android:textColor="@color/bc_white" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"/> @@ -238,10 +239,10 @@ android:paddingBottom="@dimen/primary_secondary_buttons_label_padding" android:paddingTop="@dimen/primary_secondary_buttons_label_padding" android:gravity="center" - android:background="@drawable/shape_red_outlined_button_background" + android:background="@drawable/squircle_red_outlined_button_background" android:text="@string/call_dialog_zrtp_validate_trust_letters_do_not_match" android:textSize="13sp" - android:textColor="?attr/color_danger_500" + android:textColor="@color/danger_500" android:maxLines="1" android:ellipsize="end" app:layout_constraintWidth_max="@dimen/button_max_width" diff --git a/app/src/main/res/layout/dialog_zrtp_security_alert.xml b/app/src/main/res/layout/dialog_zrtp_security_alert.xml index 6115b7026a..f2044b5359 100644 --- a/app/src/main/res/layout/dialog_zrtp_security_alert.xml +++ b/app/src/main/res/layout/dialog_zrtp_security_alert.xml @@ -116,7 +116,7 @@ android:paddingBottom="@dimen/primary_secondary_buttons_label_padding" android:paddingTop="@dimen/primary_secondary_buttons_label_padding" android:gravity="center" - android:background="@drawable/shape_red_button_background" + android:background="@drawable/squircle_red_button_background" android:text="@string/call_action_hang_up" android:textSize="13sp" android:textColor="@color/bc_white" diff --git a/app/src/main/res/layout/drawer_menu.xml b/app/src/main/res/layout/drawer_menu.xml index ac7566c8d9..efa4817d98 100644 --- a/app/src/main/res/layout/drawer_menu.xml +++ b/app/src/main/res/layout/drawer_menu.xml @@ -31,8 +31,8 @@ + android:layout_marginBottom="10dp" + android:background="@drawable/squircle_transparent_button_background"> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="@id/top_bar_background" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="@id/top_bar_background" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="@id/top_bar_background" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="@id/top_bar_barrier" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="@id/top_bar_barrier" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="@id/top_bar_barrier" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="@id/top_bar_background" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="@id/top_bar_background" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="@id/top_bar_background" /> @@ -244,13 +245,14 @@ android:onClick="@{() -> viewModel.toggleAutoAnswerExpand()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" - android:layout_marginTop="20dp" + android:padding="10dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="10dp" android:text="@string/settings_advanced_auto_answer_title" android:drawableEnd="@{viewModel.expandAutoAnswer ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/early_media_settings"/> @@ -273,13 +275,14 @@ android:onClick="@{() -> viewModel.toggleAudioCodecsExpand()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" - android:layout_marginTop="20dp" + android:padding="10dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="10dp" android:text="@string/settings_advanced_audio_codecs_title" android:drawableEnd="@{viewModel.expandAudioCodecs ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/auto_answer_settings"/> @@ -307,13 +310,14 @@ android:onClick="@{() -> viewModel.toggleVideoCodecsExpand()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" - android:layout_marginTop="20dp" + android:padding="10dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="10dp" android:text="@string/settings_advanced_video_codecs_title" android:drawableEnd="@{viewModel.expandVideoCodecs ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/audio_codecs"/> diff --git a/app/src/main/res/layout/settings_advanced_fragment.xml b/app/src/main/res/layout/settings_advanced_fragment.xml index 4d61d7f072..fba5ec3483 100644 --- a/app/src/main/res/layout/settings_advanced_fragment.xml +++ b/app/src/main/res/layout/settings_advanced_fragment.xml @@ -237,13 +237,14 @@ android:onClick="@{() -> viewModel.toggleAudioDevicesExpand()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" - android:layout_marginTop="20dp" + android:padding="10dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="10dp" android:text="@string/settings_advanced_audio_devices_title" android:drawableEnd="@{viewModel.expandAudioDevices ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/download_and_apply"/> @@ -377,6 +378,7 @@ android:text="@string/settings_advanced_go_to_android_app_settings_title" android:drawableEnd="@drawable/arrow_square_out" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintHorizontal_bias="1" app:layout_constraintVertical_bias="0" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/settings_calls.xml b/app/src/main/res/layout/settings_calls.xml index 3a0159b4b8..81dfde71d1 100644 --- a/app/src/main/res/layout/settings_calls.xml +++ b/app/src/main/res/layout/settings_calls.xml @@ -227,6 +227,7 @@ android:text="@string/settings_calls_change_ringtone_title" android:drawableEnd="@drawable/arrow_square_out" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintHorizontal_bias="1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -246,6 +247,7 @@ android:text="@string/settings_advanced_calls" android:drawableEnd="@drawable/caret_right" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintHorizontal_bias="1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/settings_contacts.xml b/app/src/main/res/layout/settings_contacts.xml index ee8d39babc..83c367768a 100644 --- a/app/src/main/res/layout/settings_contacts.xml +++ b/app/src/main/res/layout/settings_contacts.xml @@ -134,6 +134,7 @@ android:ellipsize="end" android:drawableEnd="@drawable/caret_right" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.ldapAvailable ? View.VISIBLE : View.GONE}" app:layout_constraintTop_toBottomOf="@id/hide_empty_contacts_switch" app:layout_constraintBottom_toTopOf="@id/existing_ldap_servers" @@ -170,6 +171,7 @@ android:ellipsize="end" android:drawableEnd="@drawable/caret_right" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintTop_toBottomOf="@id/existing_ldap_servers" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> diff --git a/app/src/main/res/layout/settings_contacts_carddav_ldap_list_cell.xml b/app/src/main/res/layout/settings_contacts_carddav_ldap_list_cell.xml index 9ca7274f2a..ebc698f5fc 100644 --- a/app/src/main/res/layout/settings_contacts_carddav_ldap_list_cell.xml +++ b/app/src/main/res/layout/settings_contacts_carddav_ldap_list_cell.xml @@ -24,6 +24,7 @@ android:ellipsize="end" android:drawableEnd="@drawable/pencil_simple" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintStart_toEndOf="@id/avatar" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent"/> diff --git a/app/src/main/res/layout/settings_fragment.xml b/app/src/main/res/layout/settings_fragment.xml index aeaa10d6b1..c2895286a8 100644 --- a/app/src/main/res/layout/settings_fragment.xml +++ b/app/src/main/res/layout/settings_fragment.xml @@ -75,13 +75,14 @@ android:onClick="@{() -> viewModel.toggleSecurityExpand()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" - android:layout_marginTop="16dp" + android:padding="10dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="10dp" android:text="@string/settings_security_title" android:drawableEnd="@{viewModel.expandSecurity ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent"/> @@ -105,13 +106,14 @@ android:onClick="@{() -> viewModel.toggleCallsExpand()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" - android:layout_marginTop="16dp" + android:padding="10dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="10dp" android:text="@string/settings_calls_title" android:drawableEnd="@{viewModel.expandCalls ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/security_settings"/> @@ -135,13 +137,14 @@ android:onClick="@{() -> viewModel.toggleConversationsExpand()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" - android:layout_marginTop="16dp" + android:padding="10dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="10dp" android:text="@string/settings_conversations_title" android:drawableEnd="@{viewModel.expandConversations ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.showConversationsSettings ? View.VISIBLE : View.GONE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -165,13 +168,14 @@ android:onClick="@{() -> viewModel.toggleContactsExpand()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" - android:layout_marginTop="16dp" + android:padding="10dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="10dp" android:text="@string/settings_contacts_title" android:drawableEnd="@{viewModel.expandContacts ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.showContactsSettings ? View.VISIBLE : View.GONE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -195,13 +199,14 @@ android:onClick="@{() -> viewModel.toggleMeetingsExpand()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" - android:layout_marginTop="16dp" + android:padding="10dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="10dp" android:text="@string/settings_meetings_title" android:drawableEnd="@{viewModel.expandMeetings ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.showMeetingsSettings ? View.VISIBLE : View.GONE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -225,13 +230,14 @@ android:onClick="@{() -> viewModel.toggleNetworkExpand()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" - android:layout_marginTop="16dp" + android:padding="10dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="10dp" android:text="@string/settings_network_title" android:drawableEnd="@{viewModel.expandNetwork ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/meetings_settings"/> @@ -254,13 +260,14 @@ android:onClick="@{() -> viewModel.toggleUserInterfaceExpand()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" - android:layout_marginTop="16dp" + android:padding="10dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="10dp" android:text="@string/settings_user_interface_title" android:drawableEnd="@{viewModel.expandUserInterface ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/network_settings"/> @@ -283,14 +290,15 @@ android:onClick="@{() -> viewModel.toggleTunnelExpand()}" android:layout_width="0dp" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" - android:layout_marginTop="16dp" + android:padding="10dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="10dp" android:text="@string/settings_tunnel_title" android:visibility="@{viewModel.isTunnelAvailable ? View.VISIBLE : View.GONE}" android:drawableEnd="@{viewModel.expandTunnel ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/user_interface_settings"/> @@ -313,14 +321,15 @@ android:onClick="@{advancedSettingsClickListener}" android:layout_width="0dp" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" - android:layout_marginTop="16dp" + android:padding="10dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="10dp" android:layout_marginBottom="@dimen/screen_bottom_margin" android:text="@string/settings_advanced_title" android:drawableEnd="@drawable/caret_right" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.showAdvancedSettings ? View.VISIBLE : View.GONE}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -332,14 +341,15 @@ android:onClick="@{developerSettingsClickListener}" android:layout_width="0dp" android:layout_height="wrap_content" - android:padding="5dp" - android:layout_marginStart="26dp" - android:layout_marginEnd="26dp" - android:layout_marginTop="16dp" + android:padding="10dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginTop="10dp" android:layout_marginBottom="@dimen/screen_bottom_margin" android:text="@string/settings_developer_title" android:drawableEnd="@drawable/caret_right" android:drawableTint="?attr/color_main2_600" + android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.showDeveloperSettings ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintVertical_bias="0" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/start_call_fragment.xml b/app/src/main/res/layout/start_call_fragment.xml index 8bbb4d0c5e..484aca5a3b 100644 --- a/app/src/main/res/layout/start_call_fragment.xml +++ b/app/src/main/res/layout/start_call_fragment.xml @@ -186,7 +186,7 @@ android:id="@+id/gradient_background" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginStart="22dp" + android:layout_marginStart="@dimen/icon_size" android:background="@drawable/shape_gradient" app:layout_constraintBottom_toBottomOf="@id/group_call_icon" app:layout_constraintEnd_toEndOf="@id/group_call_label" @@ -196,8 +196,8 @@ + app:tint="@color/bc_white" /> diff --git a/app/src/main/res/layout/start_chat_fragment.xml b/app/src/main/res/layout/start_chat_fragment.xml index 86b77a41ef..b5b26e9f1d 100644 --- a/app/src/main/res/layout/start_chat_fragment.xml +++ b/app/src/main/res/layout/start_chat_fragment.xml @@ -168,7 +168,7 @@ android:id="@+id/gradient_background" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginStart="22dp" + android:layout_marginStart="@dimen/icon_size" android:background="@drawable/shape_gradient" app:layout_constraintBottom_toBottomOf="@id/group_chat_icon" app:layout_constraintEnd_toEndOf="@id/group_chat_label" @@ -178,8 +178,8 @@ @color/gray_main2_600 @color/gray_600 - @color/background_color_alt_dark_mode + @color/gray_main2_800 @color/background_color_alt_dark_mode @color/gray_main2_600 diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 45a8548271..506d7b2f88 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -11,6 +11,7 @@ 14dp 24dp + 32dp 48dp 100dp 30dp From 3cec19126d32fa7ca0bac98480ab51a620363a60 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 12 Jan 2026 15:23:29 +0100 Subject: [PATCH 399/593] Fixed contacts not updated with LDAP results if latest query fails --- app/src/main/java/org/linphone/contacts/ContactsManager.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 14b07cb680..3335742f87 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -96,8 +96,6 @@ class ContactsManager private val magicSearchListener = object : MagicSearchListenerStub() { @WorkerThread override fun onSearchResultsReceived(magicSearch: MagicSearch) { - reloadRemoteContactsJob?.cancel() - var queriedSipUri = "" for ((key, value) in magicSearchMap.entries) { if (value == magicSearch) { @@ -121,6 +119,7 @@ class ContactsManager Log.w("$TAG Received friend [${friend.name}] with SIP URI [$address] doesn't match queried SIP URI [$queriedSipUri]") } else { found = true + reloadRemoteContactsJob?.cancel() // Store friend in app's cache to be re-used in call history, conversations, etc... val temporaryFriendList = getRemoteContactDirectoriesCacheFriendList() From 4039da9c8addc0781416a593580a4bbe3a9d7f99 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 12 Jan 2026 16:19:33 +0100 Subject: [PATCH 400/593] Improved meeting layout --- app/src/main/res/layout/meeting_fragment.xml | 53 ++++++++------------ 1 file changed, 20 insertions(+), 33 deletions(-) diff --git a/app/src/main/res/layout/meeting_fragment.xml b/app/src/main/res/layout/meeting_fragment.xml index d6a9157fe8..5c43e081cd 100644 --- a/app/src/main/res/layout/meeting_fragment.xml +++ b/app/src/main/res/layout/meeting_fragment.xml @@ -109,8 +109,8 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="16dp" - android:layout_marginStart="16dp" - android:layout_marginEnd="16dp" + android:layout_marginStart="10dp" + android:layout_marginEnd="10dp" android:text="@{viewModel.subject, default=`Broadcast about agility in software development`}" android:textSize="20sp" android:textColor="?attr/color_main2_600" @@ -138,38 +138,25 @@ - - + app:layout_constraintEnd_toEndOf="parent" /> Date: Mon, 12 Jan 2026 16:44:25 +0100 Subject: [PATCH 401/593] Fixed generated avatar size sometimes small --- .../java/org/linphone/utils/DataBindingUtils.kt | 2 +- app/src/main/java/org/linphone/utils/ImageUtils.kt | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 24134db9ba..8bd5eb08cc 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -500,7 +500,7 @@ private fun getErrorImageLoader( R.drawable.inset_user_circle } } else { - ImageUtils.generatedAvatarIfNeededAndReturnPath(context, size, textSize, initials) + ImageUtils.generatedAvatarIfNeededAndReturnPath(context, initials) } } diff --git a/app/src/main/java/org/linphone/utils/ImageUtils.kt b/app/src/main/java/org/linphone/utils/ImageUtils.kt index 1367b8cd0c..1eb957ac6e 100644 --- a/app/src/main/java/org/linphone/utils/ImageUtils.kt +++ b/app/src/main/java/org/linphone/utils/ImageUtils.kt @@ -35,13 +35,14 @@ import org.linphone.contacts.AvatarGenerator import org.linphone.core.tools.Log import androidx.core.net.toUri import androidx.core.graphics.createBitmap +import org.linphone.R class ImageUtils { companion object { private const val TAG = "[Image Utils]" @AnyThread - fun generatedAvatarIfNeededAndReturnPath(context: Context, size: Int = 0, textSize: Int = 0, initials: String): String { + fun generatedAvatarIfNeededAndReturnPath(context: Context, initials: String): String { val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES val suffix = if (darkMode) "_dark" else "_light" @@ -53,14 +54,9 @@ class ImageUtils { val builder = AvatarGenerator(context) builder.setInitials(initials) - if (size > 0) { - builder.setAvatarSize( - AppUtils.getDimension(size).toInt() - ) - } - if (textSize > 0) { - builder.setTextSize(AppUtils.getDimension(textSize)) - } + builder.setAvatarSize(AppUtils.getDimension(R.dimen.avatar_big_size).toInt()) + builder.setTextSize(AppUtils.getDimension(R.dimen.avatar_initials_call_text_size)) + val bitmap = builder.buildBitmap(false) val path = FileUtils.storeBitmap(bitmap, generatedAvatarPath) return path From b574bf420cf3eae6c70253a82a107cafce4e16c4 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 13 Jan 2026 10:20:52 +0100 Subject: [PATCH 402/593] Fixed file viewer hover color --- app/src/main/res/color/file_viewer_main2_500.xml | 6 ------ .../circle_transparent_light_button_background.xml | 9 +++++++++ .../drawable/shape_circle_light_pressed_background.xml | 5 +++++ app/src/main/res/layout/file_media_viewer_activity.xml | 7 +++++-- app/src/main/res/layout/file_viewer_activity.xml | 7 +++++-- app/src/main/res/layout/meeting_fragment.xml | 5 +++++ app/src/main/res/layout/recording_player_fragment.xml | 7 +++++-- 7 files changed, 34 insertions(+), 12 deletions(-) delete mode 100644 app/src/main/res/color/file_viewer_main2_500.xml create mode 100644 app/src/main/res/drawable/circle_transparent_light_button_background.xml create mode 100644 app/src/main/res/drawable/shape_circle_light_pressed_background.xml diff --git a/app/src/main/res/color/file_viewer_main2_500.xml b/app/src/main/res/color/file_viewer_main2_500.xml deleted file mode 100644 index 83e5a29594..0000000000 --- a/app/src/main/res/color/file_viewer_main2_500.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/circle_transparent_light_button_background.xml b/app/src/main/res/drawable/circle_transparent_light_button_background.xml new file mode 100644 index 0000000000..2957e4ec3c --- /dev/null +++ b/app/src/main/res/drawable/circle_transparent_light_button_background.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_circle_light_pressed_background.xml b/app/src/main/res/drawable/shape_circle_light_pressed_background.xml new file mode 100644 index 0000000000..fb7df193e4 --- /dev/null +++ b/app/src/main/res/drawable/shape_circle_light_pressed_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/file_media_viewer_activity.xml b/app/src/main/res/layout/file_media_viewer_activity.xml index 466d6f1a68..b20cc7e3a5 100644 --- a/app/src/main/res/layout/file_media_viewer_activity.xml +++ b/app/src/main/res/layout/file_media_viewer_activity.xml @@ -62,6 +62,7 @@ android:layout_width="wrap_content" android:layout_height="0dp" android:src="@drawable/caret_left" + android:background="@drawable/circle_transparent_light_button_background" android:contentDescription="@string/content_description_go_back_icon" app:tint="?attr/color_main1_500" app:layout_constraintStart_toStartOf="parent" @@ -111,8 +112,9 @@ android:layout_height="0dp" android:src="@drawable/share_network" android:contentDescription="@string/content_description_share_file" + android:background="@drawable/circle_transparent_light_button_background" android:visibility="@{sharedViewModel.mediaViewerFullScreenMode || viewModel.isCurrentlyDisplayedFileFromEphemeralMessage ? View.GONE : View.VISIBLE}" - app:tint="@color/file_viewer_main2_500" + app:tint="@color/gray_main2_600" app:layout_constraintEnd_toStartOf="@id/save" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="@id/top_bar_background" /> @@ -124,9 +126,10 @@ android:layout_width="wrap_content" android:layout_height="0dp" android:src="@drawable/download_simple" + android:background="@drawable/circle_transparent_light_button_background" android:contentDescription="@string/content_description_save_file" android:visibility="@{sharedViewModel.mediaViewerFullScreenMode || viewModel.isCurrentlyDisplayedFileFromEphemeralMessage ? View.GONE : View.VISIBLE}" - app:tint="@color/file_viewer_main2_500" + app:tint="@color/gray_main2_600" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="@id/top_bar_background" /> diff --git a/app/src/main/res/layout/file_viewer_activity.xml b/app/src/main/res/layout/file_viewer_activity.xml index e236244418..328f6793aa 100644 --- a/app/src/main/res/layout/file_viewer_activity.xml +++ b/app/src/main/res/layout/file_viewer_activity.xml @@ -100,6 +100,7 @@ android:layout_width="wrap_content" android:layout_height="0dp" android:src="@drawable/caret_left" + android:background="@drawable/circle_transparent_light_button_background" android:contentDescription="@string/content_description_go_back_icon" app:tint="?attr/color_main1_500" app:layout_constraintStart_toStartOf="parent" @@ -148,9 +149,10 @@ android:layout_width="wrap_content" android:layout_height="0dp" android:src="@drawable/share_network" + android:background="@drawable/circle_transparent_light_button_background" android:contentDescription="@string/content_description_share_file" android:visibility="@{viewModel.fullScreenMode || viewModel.isFromEphemeralMessage ? View.GONE : View.VISIBLE}" - app:tint="@color/file_viewer_main2_500" + app:tint="@color/gray_main2_600" app:layout_constraintEnd_toStartOf="@id/save" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="@id/top_bar_barrier" /> @@ -162,9 +164,10 @@ android:layout_width="wrap_content" android:layout_height="0dp" android:src="@drawable/download_simple" + android:background="@drawable/circle_transparent_light_button_background" android:contentDescription="@string/content_description_save_file" android:visibility="@{viewModel.fullScreenMode || viewModel.isFromEphemeralMessage ? View.GONE : View.VISIBLE}" - app:tint="@color/file_viewer_main2_500" + app:tint="@color/gray_main2_600" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="@id/top_bar_barrier" /> diff --git a/app/src/main/res/layout/meeting_fragment.xml b/app/src/main/res/layout/meeting_fragment.xml index 5c43e081cd..f5892e6c0b 100644 --- a/app/src/main/res/layout/meeting_fragment.xml +++ b/app/src/main/res/layout/meeting_fragment.xml @@ -114,6 +114,7 @@ android:text="@{viewModel.subject, default=`Broadcast about agility in software development`}" android:textSize="20sp" android:textColor="?attr/color_main2_600" + android:includeFontPadding="false" android:maxLines="2" android:ellipsize="end" android:drawableStart="@{viewModel.isBroadcast ? @drawable/slideshow : @drawable/video_conference, default=@drawable/video_conference}" @@ -147,6 +148,7 @@ android:text="@{viewModel.sipUri, default=`linphone.com/wjre.fr`}" android:textSize="14sp" android:textColor="?attr/color_main2_600" + android:includeFontPadding="false" android:maxLines="1" android:ellipsize="end" android:drawableStart="@drawable/video_camera" @@ -169,6 +171,7 @@ android:text="@{viewModel.dateTime, default=`October 11th, 2023 | 17:00 - 18:00`}" android:textSize="14sp" android:textColor="?attr/color_main2_600" + android:includeFontPadding="false" android:maxLines="1" android:ellipsize="end" android:drawableStart="@drawable/clock" @@ -189,6 +192,7 @@ android:text="@{viewModel.timezone, default=@string/meeting_schedule_timezone_title}" android:textSize="14sp" android:textColor="?attr/color_main2_600" + android:includeFontPadding="false" android:maxLines="1" android:ellipsize="end" android:drawableStart="@drawable/globe_hemisphere_west" @@ -220,6 +224,7 @@ android:text="@{viewModel.description, default=`Lorem ipsum dolor sit amet`}" android:textSize="14sp" android:textColor="?attr/color_main2_600" + android:includeFontPadding="false" android:maxLines="3" android:ellipsize="end" android:drawableStart="@drawable/file_text" diff --git a/app/src/main/res/layout/recording_player_fragment.xml b/app/src/main/res/layout/recording_player_fragment.xml index f1ce79aebf..6ade266808 100644 --- a/app/src/main/res/layout/recording_player_fragment.xml +++ b/app/src/main/res/layout/recording_player_fragment.xml @@ -118,6 +118,7 @@ android:layout_width="wrap_content" android:layout_height="0dp" android:src="@drawable/caret_left" + android:background="@drawable/circle_transparent_light_button_background" android:contentDescription="@string/content_description_go_back_icon" app:tint="?attr/color_main1_500" app:layout_constraintStart_toStartOf="parent" @@ -167,8 +168,9 @@ android:layout_height="0dp" android:src="@drawable/share_network" android:contentDescription="@string/content_description_share_file" + android:background="@drawable/circle_transparent_light_button_background" android:visibility="@{viewModel.isUsingSmffFileFormat ? View.GONE : View.VISIBLE}" - app:tint="@color/file_viewer_main2_500" + app:tint="@color/gray_main2_600" app:layout_constraintEnd_toStartOf="@id/save" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="@id/top_bar_background" /> @@ -180,9 +182,10 @@ android:layout_width="wrap_content" android:layout_height="0dp" android:src="@drawable/download_simple" + android:background="@drawable/circle_transparent_light_button_background" android:contentDescription="@string/content_description_save_file" android:visibility="@{viewModel.isUsingSmffFileFormat ? View.GONE : View.VISIBLE}" - app:tint="@color/file_viewer_main2_500" + app:tint="@color/gray_main2_600" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="@id/top_bar_background" /> From 50c922b581fad6e912678c186a9aafdfc88cf234 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 13 Jan 2026 15:46:45 +0100 Subject: [PATCH 403/593] Small UI changes to improve look & feel --- .../res/color/main_top_bar_icon_color.xml | 9 +++++++ .../res/layout-land/call_active_fragment.xml | 3 ++- .../res/layout-land/main_activity_top_bar.xml | 25 ++++++++----------- .../res/layout/account_advanced_settings.xml | 2 ++ .../call_active_conference_fragment.xml | 3 ++- .../main/res/layout/call_active_fragment.xml | 3 ++- .../layout/chat_conversation_send_area.xml | 4 +-- .../main/res/layout/main_activity_top_bar.xml | 25 ++++++++----------- app/src/main/res/values/styles.xml | 10 ++++++++ 9 files changed, 49 insertions(+), 35 deletions(-) create mode 100644 app/src/main/res/color/main_top_bar_icon_color.xml diff --git a/app/src/main/res/color/main_top_bar_icon_color.xml b/app/src/main/res/color/main_top_bar_icon_color.xml new file mode 100644 index 0000000000..f3f3704e88 --- /dev/null +++ b/app/src/main/res/color/main_top_bar_icon_color.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/layout-land/call_active_fragment.xml b/app/src/main/res/layout-land/call_active_fragment.xml index b3e63784d3..2eb8f0a6c8 100644 --- a/app/src/main/res/layout-land/call_active_fragment.xml +++ b/app/src/main/res/layout-land/call_active_fragment.xml @@ -156,7 +156,7 @@ app:layout_constraintStart_toEndOf="@id/back" app:layout_constraintEnd_toStartOf="@id/separator" app:layout_constraintTop_toTopOf="@id/back" - app:layout_constraintBottom_toBottomOf="@id/back"/> + app:layout_constraintBottom_toTopOf="@id/call_media_encryption_info"/> diff --git a/app/src/main/res/layout-land/main_activity_top_bar.xml b/app/src/main/res/layout-land/main_activity_top_bar.xml index cc52335a72..e025dc7469 100644 --- a/app/src/main/res/layout-land/main_activity_top_bar.xml +++ b/app/src/main/res/layout-land/main_activity_top_bar.xml @@ -47,7 +47,7 @@ app:barrierDirection="bottom" /> + app:layout_constraintBottom_toBottomOf="parent" /> + app:layout_constraintBottom_toBottomOf="parent" /> + app:layout_constraintBottom_toBottomOf="parent" /> + app:layout_constraintTop_toTopOf="@id/search" /> + app:layout_constraintTop_toTopOf="@id/search" /> diff --git a/app/src/main/res/layout/account_advanced_settings.xml b/app/src/main/res/layout/account_advanced_settings.xml index 6d7581f253..2ca3aa8a49 100644 --- a/app/src/main/res/layout/account_advanced_settings.xml +++ b/app/src/main/res/layout/account_advanced_settings.xml @@ -423,6 +423,8 @@ android:text="@string/account_settings_update_password_title" android:maxLines="1" android:ellipsize="end" + android:drawableEnd="@drawable/pencil_simple" + android:background="@drawable/squircle_transparent_button_background" app:layout_constraintHorizontal_bias="1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/call_active_conference_fragment.xml b/app/src/main/res/layout/call_active_conference_fragment.xml index 4cef0dbd5b..d9114b469d 100644 --- a/app/src/main/res/layout/call_active_conference_fragment.xml +++ b/app/src/main/res/layout/call_active_conference_fragment.xml @@ -99,7 +99,7 @@ app:layout_constraintStart_toEndOf="@id/back" app:layout_constraintEnd_toStartOf="@id/separator" app:layout_constraintTop_toTopOf="@id/back" - app:layout_constraintBottom_toBottomOf="@id/back"/> + app:layout_constraintBottom_toTopOf="@id/call_media_encryption_info"/> diff --git a/app/src/main/res/layout/call_active_fragment.xml b/app/src/main/res/layout/call_active_fragment.xml index 1c87530f4e..e8a14c284a 100644 --- a/app/src/main/res/layout/call_active_fragment.xml +++ b/app/src/main/res/layout/call_active_fragment.xml @@ -153,7 +153,7 @@ app:layout_constraintStart_toEndOf="@id/back" app:layout_constraintEnd_toStartOf="@id/separator" app:layout_constraintTop_toTopOf="@id/back" - app:layout_constraintBottom_toBottomOf="@id/back"/> + app:layout_constraintBottom_toTopOf="@id/call_media_encryption_info"/> diff --git a/app/src/main/res/layout/chat_conversation_send_area.xml b/app/src/main/res/layout/chat_conversation_send_area.xml index cc315ee96f..400930c0a7 100644 --- a/app/src/main/res/layout/chat_conversation_send_area.xml +++ b/app/src/main/res/layout/chat_conversation_send_area.xml @@ -147,9 +147,9 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="24dp" - android:layout_marginTop="16dp" + android:layout_marginTop="12dp" android:layout_marginEnd="5dp" - android:layout_marginBottom="16dp" + android:layout_marginBottom="12dp" android:background="@color/transparent_color" android:hint="@string/conversation_text_field_hint" android:imeOptions="flagNoPersonalizedLearning" diff --git a/app/src/main/res/layout/main_activity_top_bar.xml b/app/src/main/res/layout/main_activity_top_bar.xml index 3b6df1a89b..cd3eb9c317 100644 --- a/app/src/main/res/layout/main_activity_top_bar.xml +++ b/app/src/main/res/layout/main_activity_top_bar.xml @@ -47,7 +47,7 @@ app:barrierDirection="bottom" /> + app:layout_constraintBottom_toBottomOf="parent" /> + app:layout_constraintBottom_toBottomOf="parent" /> + app:layout_constraintBottom_toBottomOf="parent" /> + app:layout_constraintTop_toTopOf="@id/search" /> + app:layout_constraintTop_toTopOf="@id/search" /> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 7f262f2726..aded447274 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -133,6 +133,16 @@ 1 end + From 40704ccc05f0788295c6a4b119848d9c1da57b8d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 11 Feb 2026 09:19:54 +0100 Subject: [PATCH 444/593] Fixed generated APK file name not containing git version --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 675fe2cc8b..77f5781f8e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -122,7 +122,7 @@ android { variant.outputs .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } .forEach { output -> - output.outputFileName = "linphone-android-${variant.buildType.name}-${project.version}.apk" + output.outputFileName = "linphone-android-${variant.buildType.name}-${gitVersion}.apk" } } From 2ee93e088770dfc95d605609a0c07bbcd03bd87b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 11 Feb 2026 12:16:04 +0100 Subject: [PATCH 445/593] Fixed crash due to missing Crashlytics build ID in release flavor --- app/build.gradle.kts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 77f5781f8e..373b3ba633 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -122,7 +122,7 @@ android { variant.outputs .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } .forEach { output -> - output.outputFileName = "linphone-android-${variant.buildType.name}-${gitVersion}.apk" + output.outputFileName = "linphone-android-${variant.buildType.name}-$gitVersion.apk" } } @@ -172,8 +172,6 @@ android { nativeSymbolUploadEnabled = true unstrippedNativeLibsDir = path } - } else { - resValue("string", "com.crashlytics.android.build_id", "none") } buildConfigField("Boolean", "CRASHLYTICS_ENABLED", crashlyticsAvailable.toString()) } @@ -201,8 +199,6 @@ android { nativeSymbolUploadEnabled = true unstrippedNativeLibsDir = path } - } else { - resValue("string", "com.crashlytics.android.build_id", "none") } buildConfigField("Boolean", "CRASHLYTICS_ENABLED", crashlyticsAvailable.toString()) } @@ -252,7 +248,11 @@ dependencies { implementation(platform(libs.google.firebase.bom)) implementation(libs.google.firebase.messaging) - implementation(libs.google.firebase.crashlytics) + if (crashlyticsAvailable) { + implementation(libs.google.firebase.crashlytics) + } else { + compileOnly(libs.google.firebase.crashlytics) + } // https://github.com/coil-kt/coil/blob/main/LICENSE.txt Apache v2.0 implementation(libs.coil) From e8601d8daba0c5a6d0ed06dbabfe230610b37a54 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 12 Feb 2026 15:48:30 +0100 Subject: [PATCH 446/593] Fixed trusted devices label not centered above progress bar --- app/src/main/res/layout/contact_fragment.xml | 21 ++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/layout/contact_fragment.xml b/app/src/main/res/layout/contact_fragment.xml index 92172610a3..603031e901 100644 --- a/app/src/main/res/layout/contact_fragment.xml +++ b/app/src/main/res/layout/contact_fragment.xml @@ -468,7 +468,7 @@ android:layout_marginStart="3dp" android:layout_marginEnd="3dp" android:max="100" - android:progress="@{viewModel.trustedDevicesPercentage, default=0}" + android:progress="@{viewModel.trustedDevicesPercentage, default=20}" app:trackCornerRadius="50dp" app:trackThickness="22dp" app:trackColor="@color/transparent_color" @@ -479,6 +479,16 @@ app:layout_constraintTop_toTopOf="@id/trusted_devices_progress_background" app:layout_constraintBottom_toBottomOf="@id/trusted_devices_progress_background"/> + + From 91f13fe4073d2e0d12020c8a2527012df2878e8a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 13 Feb 2026 11:42:27 +0100 Subject: [PATCH 447/593] Fixed UI when merging calls into local conference --- .../java/org/linphone/core/CoreContext.kt | 22 ++++--- .../java/org/linphone/ui/call/CallActivity.kt | 13 ++++ .../fragment/ActiveConferenceCallFragment.kt | 11 ---- .../viewmodel/ConferenceViewModel.kt | 15 +++-- .../ui/call/fragment/ActiveCallFragment.kt | 11 ---- .../ui/call/viewmodel/CallsViewModel.kt | 60 ++++++++++++------- .../ui/call/viewmodel/CurrentCallViewModel.kt | 43 ++++++++----- .../call_activity_other_calls_top_bar.xml | 8 +-- 8 files changed, 106 insertions(+), 77 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index fc7b47aab7..420f5d0cfc 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -1040,15 +1040,21 @@ class CoreContext @WorkerThread fun terminateCall(call: Call) { - if (call.dir == Call.Dir.Incoming && LinphoneUtils.isCallIncoming(call.state)) { - val reason = if (call.core.callsNb > 1) Reason.Busy else Reason.Declined - Log.i( - "$TAG Declining call [${call.remoteAddress.asStringUriOnly()}] with reason [$reason]" - ) - call.decline(reason) + val conference = call.conference + if (conference != null) { + Log.i("$TAG Terminating conference [${call.remoteAddress.asStringUriOnly()}]") + conference.terminate() } else { - Log.i("$TAG Terminating call [${call.remoteAddress.asStringUriOnly()}]") - call.terminate() + if (call.dir == Call.Dir.Incoming && LinphoneUtils.isCallIncoming(call.state)) { + val reason = if (call.core.callsNb > 1) Reason.Busy else Reason.Declined + Log.i( + "$TAG Declining call [${call.remoteAddress.asStringUriOnly()}] with reason [$reason]" + ) + call.decline(reason) + } else { + Log.i("$TAG Terminating call [${call.remoteAddress.asStringUriOnly()}]") + call.terminate() + } } } diff --git a/app/src/main/java/org/linphone/ui/call/CallActivity.kt b/app/src/main/java/org/linphone/ui/call/CallActivity.kt index 3dc7324068..56ff88922c 100644 --- a/app/src/main/java/org/linphone/ui/call/CallActivity.kt +++ b/app/src/main/java/org/linphone/ui/call/CallActivity.kt @@ -43,6 +43,7 @@ import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController +import androidx.navigation.fragment.findNavController import androidx.window.layout.FoldingFeature import androidx.window.layout.WindowInfoTracker import androidx.window.layout.WindowLayoutInfo @@ -264,6 +265,18 @@ class CallActivity : GenericActivity() { coreContext.enableProximitySensor(enabled) } + callViewModel.goToCallEvent.observe(this) { + it.consume { + navigateToActiveCall(true) + } + } + + callViewModel.goToConferenceEvent.observe(this) { + it.consume { + navigateToActiveCall(false) + } + } + callsViewModel.showIncomingCallEvent.observe(this) { it.consume { val action = IncomingCallFragmentDirections.actionGlobalIncomingCallFragment() diff --git a/app/src/main/java/org/linphone/ui/call/conference/fragment/ActiveConferenceCallFragment.kt b/app/src/main/java/org/linphone/ui/call/conference/fragment/ActiveConferenceCallFragment.kt index 3b2973924a..58e1739350 100644 --- a/app/src/main/java/org/linphone/ui/call/conference/fragment/ActiveConferenceCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/conference/fragment/ActiveConferenceCallFragment.kt @@ -237,17 +237,6 @@ class ActiveConferenceCallFragment : GenericCallFragment() { } } - callViewModel.goToCallEvent.observe(viewLifecycleOwner) { - it.consume { - if (findNavController().currentDestination?.id == R.id.activeConferenceCallFragment) { - Log.i("$TAG Going to active call fragment") - val action = - ActiveConferenceCallFragmentDirections.actionActiveConferenceCallFragmentToActiveCallFragment() - findNavController().navigate(action) - } - } - } - binding.setBackClickListener { (requireActivity() as CallActivity).goToMainActivity() } diff --git a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt index 16cbe2ff65..5c1272cce8 100644 --- a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt @@ -98,6 +98,8 @@ class ConferenceViewModel MutableLiveData() } + var conferenceConfigured = false + private lateinit var conference: Conference private val conferenceListener = object : ConferenceListenerStub() { @@ -289,6 +291,7 @@ class ConferenceViewModel } init { + conferenceConfigured = false isPaused.value = false isConversationAvailable.value = false isMeParticipantSendingVideo.value = false @@ -297,6 +300,7 @@ class ConferenceViewModel @WorkerThread fun destroy() { + conferenceConfigured = false isCurrentCallInConference.postValue(false) if (::conference.isInitialized) { conference.removeListener(conferenceListener) @@ -314,6 +318,7 @@ class ConferenceViewModel isCurrentCallInConference.postValue(true) conference = conf conference.addListener(conferenceListener) + conferenceConfigured = true val isIn = conference.isIn val state = conf.state @@ -351,15 +356,15 @@ class ConferenceViewModel Log.w( "$TAG Conference has a participant sharing its screen, changing layout from mosaic to active speaker" ) - setNewLayout(ACTIVE_SPEAKER_LAYOUT) + setNewLayout(ACTIVE_SPEAKER_LAYOUT, call) } else if (currentLayout == AUDIO_ONLY_LAYOUT) { val defaultLayout = call.core.defaultConferenceLayout.toInt() if (defaultLayout == Conference.Layout.ActiveSpeaker.toInt()) { Log.w("$TAG Joined conference in audio only layout, switching to active speaker layout") - setNewLayout(ACTIVE_SPEAKER_LAYOUT) + setNewLayout(ACTIVE_SPEAKER_LAYOUT, call) } else { Log.w("$TAG Joined conference in audio only layout, switching to grid layout") - setNewLayout(GRID_LAYOUT) + setNewLayout(GRID_LAYOUT, call) } } } @@ -461,9 +466,9 @@ class ConferenceViewModel } @WorkerThread - fun setNewLayout(newLayout: Int) { + fun setNewLayout(newLayout: Int, currentCall: Call? = null) { if (::conference.isInitialized) { - val call = conference.call + val call = conference.call ?: currentCall if (call != null) { val params = call.core.createCallParams(call) if (params != null) { diff --git a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt index ead2822ca9..dc1933d078 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt @@ -331,17 +331,6 @@ class ActiveCallFragment : GenericCallFragment() { } } - callViewModel.goToConferenceEvent.observe(viewLifecycleOwner) { - it.consume { - if (findNavController().currentDestination?.id == R.id.activeCallFragment) { - Log.i("$TAG Going to conference fragment") - val action = - ActiveCallFragmentDirections.actionActiveCallFragmentToActiveConferenceCallFragment() - findNavController().navigate(action) - } - } - } - callViewModel.isReceivingVideo.observe(viewLifecycleOwner) { receiving -> if (!receiving && callViewModel.fullScreenMode.value == true) { Log.i("$TAG We are no longer receiving video, leaving full screen mode") diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt index cde233826a..bd58e5cdb3 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt @@ -47,6 +47,8 @@ class CallsViewModel val callsCount = MutableLiveData() + val allCallsIntoConference = MutableLiveData() + val showTopBar = MutableLiveData() val goToActiveCallEvent = MutableLiveData>() @@ -234,6 +236,7 @@ class CallsViewModel showRedToast(R.string.conference_failed_to_merge_calls_into_conference_toast, R.drawable.warning_circle) } else { conference.addParticipants(core.calls) + allCallsIntoConference.postValue(true) } } } @@ -251,9 +254,16 @@ class CallsViewModel } callsExceptCurrentOne.postValue(list) - if (core.callsNb > 1) { - showTopBar.postValue(true) - if (core.callsNb == 2) { + val callsCount = core.callsNb + if (callsCount > 1) { + val callsNotInConference = core.calls.filter { + it.conference == null + } + val callsNotInConferenceCount = callsNotInConference.count() + Log.i("$TAG Found [$callsNotInConferenceCount] calls not in conference over [$callsCount] calls") + allCallsIntoConference.postValue(callsNotInConferenceCount == 0) + + if (callsNotInConferenceCount == 1) { val found = core.calls.find { it.state == Call.State.Paused } @@ -273,33 +283,37 @@ class CallsViewModel } callsTopBarStatus.postValue(LinphoneUtils.callStateToString(found.state)) } else { - Log.e("$TAG Failed to find a paused call") + Log.w("$TAG Failed to find a paused call") } - } else { + } else if (callsNotInConferenceCount > 1) { callsTopBarLabel.postValue( AppUtils.getFormattedString(R.string.calls_paused_count_label, core.callsNb - 1) ) callsTopBarStatus.postValue("") // TODO: improve ? + } else { + configureTopBarForSingleCallOrConference() } - } else { - if (core.callsNb == 1) { - callsTopBarIcon.postValue(R.drawable.phone) + } else if (core.callsNb == 1) { + configureTopBarForSingleCallOrConference() + } + } - val call = core.calls.first() - val conference = call.conference - if (conference != null) { - callsTopBarLabel.postValue(conference.subject) - } else { - val remoteAddress = call.callLog.remoteAddress - val contact = coreContext.contactsManager.findContactByAddress( - remoteAddress - ) - callsTopBarLabel.postValue( - contact?.name ?: LinphoneUtils.getDisplayName(remoteAddress) - ) - } - callsTopBarStatus.postValue(LinphoneUtils.callStateToString(call.state)) - } + private fun configureTopBarForSingleCallOrConference() { + callsTopBarIcon.postValue(R.drawable.phone) + + val call = coreContext.core.calls.first() + val conference = call.conference + if (conference != null) { + callsTopBarLabel.postValue(conference.subject) + } else { + val remoteAddress = call.callLog.remoteAddress + val contact = coreContext.contactsManager.findContactByAddress( + remoteAddress + ) + callsTopBarLabel.postValue( + contact?.name ?: LinphoneUtils.getDisplayName(remoteAddress) + ) } + callsTopBarStatus.postValue(LinphoneUtils.callStateToString(call.state)) } } diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 35af7fb3e8..b92378136c 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -392,6 +392,13 @@ class CurrentCallViewModel } else -> {} } + + if (call.conference != null && !conferenceModel.conferenceConfigured) { + Log.i("$TAG Found conference on call but not conference model, initializing it now") + conferenceModel.configureFromCall(call) + updateMicrophoneMutedIcon() + goToConferenceEvent.postValue(Event(true)) + } } } @@ -695,21 +702,7 @@ class CurrentCallViewModel @UiThread fun refreshMicrophoneState() { coreContext.postOnCoreThread { - if (::currentCall.isInitialized) { - val micMuted = if (currentCall.conference != null) { - currentCall.conference?.microphoneMuted == true - } else { - currentCall.microphoneMuted - } - if (micMuted != isMicrophoneMuted.value) { - if (micMuted) { - Log.w("$TAG Microphone is muted, updating button state accordingly") - } else { - Log.i("$TAG Microphone is not muted, updating button state accordingly") - } - isMicrophoneMuted.postValue(micMuted) - } - } + updateMicrophoneMutedIcon() } } @@ -1097,6 +1090,7 @@ class CurrentCallViewModel conferenceModel.configureFromCall(call) goToConferenceEvent.postValue(Event(true)) } else { + Log.i("$TAG No conference attached to this call, going to call fragment") conferenceModel.destroy() goToCallEvent.postValue(Event(true)) } @@ -1262,6 +1256,25 @@ class CurrentCallViewModel } } + @WorkerThread + private fun updateMicrophoneMutedIcon() { + if (::currentCall.isInitialized) { + val micMuted = if (currentCall.conference != null) { + currentCall.conference?.microphoneMuted == true + } else { + currentCall.microphoneMuted + } + if (micMuted != isMicrophoneMuted.value) { + if (micMuted) { + Log.w("$TAG Microphone is muted, updating button state accordingly") + } else { + Log.i("$TAG Microphone is not muted, updating button state accordingly") + } + isMicrophoneMuted.postValue(micMuted) + } + } + } + @WorkerThread private fun updateOutputAudioDevice(audioDevice: AudioDevice?) { Log.i("$TAG Output audio device updated to [${audioDevice?.deviceName} (${audioDevice?.type})]") diff --git a/app/src/main/res/layout/call_activity_other_calls_top_bar.xml b/app/src/main/res/layout/call_activity_other_calls_top_bar.xml index c4cded125d..af0093fe9d 100644 --- a/app/src/main/res/layout/call_activity_other_calls_top_bar.xml +++ b/app/src/main/res/layout/call_activity_other_calls_top_bar.xml @@ -13,7 +13,7 @@ Date: Fri, 13 Feb 2026 15:18:42 +0100 Subject: [PATCH 448/593] Switched to chatRoom.subjectUft8 + fixed notification & shortcut label when group chat room subject is updated --- .../notifications/NotificationsManager.kt | 18 ++++++++++++++---- .../ui/call/viewmodel/CurrentCallViewModel.kt | 2 +- .../ui/main/chat/model/ConversationModel.kt | 14 +++++++------- .../viewmodel/AbstractConversationViewModel.kt | 2 +- .../ConversationForwardMessageViewModel.kt | 2 +- .../viewmodel/ConversationInfoViewModel.kt | 18 +++++++++--------- .../chat/viewmodel/ConversationViewModel.kt | 10 +++++----- .../viewmodel/ConversationsListViewModel.kt | 2 +- .../viewmodel/StartConversationViewModel.kt | 2 +- .../contacts/viewmodel/ContactViewModel.kt | 2 +- .../main/history/viewmodel/HistoryViewModel.kt | 2 +- .../viewmodel/AddressSelectionViewModel.kt | 6 +++--- .../java/org/linphone/utils/ShortcutUtils.kt | 2 +- 13 files changed, 46 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 1240e989d8..779bf9e4f7 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -492,6 +492,13 @@ class NotificationsManager Log.i("$TAG A message has been edited, checking if notification should be updated") updateConversationNotification(chatRoom, message) } + + override fun onChatRoomSubjectChanged(core: Core, chatRoom: ChatRoom) { + if (ShortcutUtils.isShortcutToChatRoomAlreadyCreated(coreContext.context, chatRoom)) { + Log.i("$TAG Updating chat room shortcut with new subject [${chatRoom.subjectUtf8}]") + ShortcutUtils.createOrUpdateChatRoomShortcut(coreContext.context, chatRoom) + } + } } val chatMessageListener: ChatMessageListener = object : ChatMessageListenerStub() { @@ -1019,7 +1026,7 @@ class NotificationsManager notifiable.isGroup = false } else { notifiable.isGroup = true - notifiable.groupTitle = chatRoom.subject + notifiable.groupTitle = chatRoom.subjectUtf8 } for (message in chatRoom.unreadHistory) { @@ -1028,6 +1035,9 @@ class NotificationsManager notifiable.messages.add(notifiableMessage) } } else { + // Update notification subject in case it has changed since last message + notifiable.groupTitle = chatRoom.subjectUtf8 + for (message in messages) { if (message.isRead || message.isOutgoing) continue val notifiableMessage = getNotifiableForChatMessage(message) @@ -1044,11 +1054,11 @@ class NotificationsManager val notifiable = getNotifiableForConversation(chatRoom, messages) if (!chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) { - if (chatRoom.subject != notifiable.groupTitle) { + if (chatRoom.subjectUtf8 != notifiable.groupTitle) { Log.i( - "$TAG Updating notification subject from [${notifiable.groupTitle}] to [${chatRoom.subject}]" + "$TAG Updating notification subject from [${notifiable.groupTitle}] to [${chatRoom.subjectUtf8}]" ) - notifiable.groupTitle = chatRoom.subject + notifiable.groupTitle = chatRoom.subjectUtf8 } } diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index b92378136c..fc410ab76c 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -421,7 +421,7 @@ class CurrentCallViewModel if (state == ChatRoom.State.Instantiated) return val id = LinphoneUtils.getConversationId(chatRoom) - Log.i("$TAG Conversation [$id] (${chatRoom.subject}) state changed: [$state]") + Log.i("$TAG Conversation [$id] (${chatRoom.subjectUtf8}) state changed: [$state]") if (state == ChatRoom.State.Created) { Log.i("$TAG Conversation [$id] successfully created") diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt index 3cf3298223..29e7a097fd 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt @@ -101,7 +101,7 @@ class ConversationModel override fun onStateChanged(chatRoom: ChatRoom, newState: ChatRoom.State?) { Log.i("$TAG Conversation state changed [${chatRoom.state}]") if (chatRoom.state == ChatRoom.State.Created) { - subject.postValue(chatRoom.subject) + subject.postValue(chatRoom.subjectUtf8) computeParticipants() } else if (chatRoom.state == ChatRoom.State.Deleted) { Log.i("$TAG Conversation [$id] has been deleted") @@ -113,7 +113,7 @@ class ConversationModel override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) { // This is required as a Created chat room may not have the participants list yet Log.i("$TAG Conversation has been joined") - subject.postValue(chatRoom.subject) + subject.postValue(chatRoom.subjectUtf8) computeParticipants() } @@ -156,8 +156,8 @@ class ConversationModel @WorkerThread override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) { - Log.i("$TAG Conversation subject changed [${chatRoom.subject}]") - subject.postValue(chatRoom.subject) + Log.i("$TAG Conversation subject changed [${chatRoom.subjectUtf8}]") + subject.postValue(chatRoom.subjectUtf8) computeParticipants() } @@ -201,7 +201,7 @@ class ConversationModel chatRoom.addListener(chatRoomListener) computeComposingLabel() - subject.postValue(chatRoom.subject) + subject.postValue(chatRoom.subjectUtf8) computeParticipants() isMuted.postValue(chatRoom.muted) @@ -433,9 +433,9 @@ class ConversationModel } if (isGroup) { - if (avatarModel.value == null || avatarModel.value?.contactName != chatRoom.subject) { + if (avatarModel.value == null || avatarModel.value?.contactName != chatRoom.subjectUtf8) { val fakeFriend = coreContext.core.createFriend() - fakeFriend.name = chatRoom.subject + fakeFriend.name = chatRoom.subjectUtf8 val model = ContactAvatarModel(fakeFriend) model.defaultToConversationIcon.postValue(true) model.updateSecurityLevelUsingConversation(chatRoom) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/AbstractConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/AbstractConversationViewModel.kt index b3fce51af2..0425a86fc6 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/AbstractConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/AbstractConversationViewModel.kt @@ -169,7 +169,7 @@ abstract class AbstractConversationViewModel : GenericViewModel() { return@postOnCoreThread } - val conference = LinphoneUtils.createGroupCall(account, chatRoom.subject.orEmpty()) + val conference = LinphoneUtils.createGroupCall(account, chatRoom.subjectUtf8.orEmpty()) if (conference == null) { Log.e("$TAG Failed to create group call!") showRedToast(R.string.conference_failed_to_create_group_call_toast, R.drawable.warning_circle) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationForwardMessageViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationForwardMessageViewModel.kt index fe181a3e49..ea6481c3f9 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationForwardMessageViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationForwardMessageViewModel.kt @@ -58,7 +58,7 @@ class ConversationForwardMessageViewModel if (state == ChatRoom.State.Instantiated) return val id = LinphoneUtils.getConversationId(chatRoom) - Log.i("$TAG Conversation [$id] (${chatRoom.subject}) state changed: [$state]") + Log.i("$TAG Conversation [$id] (${chatRoom.subjectUtf8}) state changed: [$state]") if (state == ChatRoom.State.Created) { Log.i("$TAG Conversation [$id] successfully created") diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt index 7cd0d0d40f..7f646e3625 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt @@ -107,7 +107,7 @@ class ConversationInfoViewModel private val chatRoomListener = object : ChatRoomListenerStub() { @WorkerThread override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) { - Log.i("$TAG A participant has been added to the group [${chatRoom.subject}]") + Log.i("$TAG A participant has been added to the group [${chatRoom.subjectUtf8}]") val message = AppUtils.getFormattedString( R.string.conversation_info_participant_added_to_conversation_toast, getParticipant(eventLog) @@ -120,7 +120,7 @@ class ConversationInfoViewModel @WorkerThread override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) { - Log.i("$TAG A participant has been removed from the group [${chatRoom.subject}]") + Log.i("$TAG A participant has been removed from the group [${chatRoom.subjectUtf8}]") val message = AppUtils.getFormattedString( R.string.conversation_info_participant_removed_from_conversation_toast, getParticipant(eventLog) @@ -134,7 +134,7 @@ class ConversationInfoViewModel @WorkerThread override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) { Log.i( - "$TAG A participant has been given/removed administration rights for group [${chatRoom.subject}]" + "$TAG A participant has been given/removed administration rights for group [${chatRoom.subjectUtf8}]" ) if (eventLog.type == EventLog.Type.ConferenceParticipantSetAdmin) { val message = AppUtils.getFormattedString( @@ -156,11 +156,11 @@ class ConversationInfoViewModel @WorkerThread override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) { Log.i( - "$TAG Conversation [${LinphoneUtils.getConversationId(chatRoom)}] has a new subject [${chatRoom.subject}]" + "$TAG Conversation [${LinphoneUtils.getConversationId(chatRoom)}] has a new subject [${chatRoom.subjectUtf8}]" ) showGreenToast(R.string.conversation_subject_changed_toast, R.drawable.check) - subject.postValue(chatRoom.subject) + subject.postValue(chatRoom.subjectUtf8) computeParticipantsList() infoChangedEvent.postValue(Event(true)) } @@ -263,7 +263,7 @@ class ConversationInfoViewModel participantsList.add(participant.address.asStringUriOnly()) } goToScheduleMeetingEvent.postValue( - Event(Pair(chatRoom.subject.orEmpty(), participantsList)) + Event(Pair(chatRoom.subjectUtf8.orEmpty(), participantsList)) ) } else { val firstParticipant = chatRoom.participants.firstOrNull() @@ -464,10 +464,10 @@ class ConversationInfoViewModel val readOnly = chatRoom.isReadOnly isReadOnly.postValue(readOnly) if (readOnly) { - Log.w("$TAG Conversation with subject [${chatRoom.subject}] is read only!") + Log.w("$TAG Conversation with subject [${chatRoom.subjectUtf8}] is read only!") } - subject.postValue(chatRoom.subject) + subject.postValue(chatRoom.subjectUtf8) peerSipUri.postValue(chatRoom.peerAddress.asStringUriOnly()) val firstParticipant = chatRoom.participants.firstOrNull() @@ -558,7 +558,7 @@ class ConversationInfoViewModel val avatar = if (groupChatRoom) { val fakeFriend = coreContext.core.createFriend() - fakeFriend.name = chatRoom.subject + fakeFriend.name = chatRoom.subjectUtf8 val model = ContactAvatarModel(fakeFriend) model.defaultToConversationIcon.postValue(true) model.updateSecurityLevelUsingConversation(chatRoom) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt index 2833c000d4..18f0188e41 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt @@ -250,7 +250,7 @@ class ConversationViewModel @WorkerThread override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) { - Log.i("$TAG Conversation subject changed [${chatRoom.subject}]") + Log.i("$TAG Conversation subject changed [${chatRoom.subjectUtf8}]") addEvents(arrayOf(eventLog)) } @@ -580,7 +580,7 @@ class ConversationViewModel if (!chatRoom.hasCapability(ChatRoom.Capabilities.Encrypted.toInt())) { if (LinphoneUtils.getAccountForAddress(chatRoom.localAddress)?.params?.instantMessagingEncryptionMandatory == true) { Log.w( - "$TAG Conversation with subject [${chatRoom.subject}] is considered as read-only because it isn't encrypted and default account is in secure mode" + "$TAG Conversation with subject [${chatRoom.subjectUtf8}] is considered as read-only because it isn't encrypted and default account is in secure mode" ) isDisabledBecauseNotSecured.postValue(true) } else { @@ -655,12 +655,12 @@ class ConversationViewModel val readOnly = chatRoom.isReadOnly isReadOnly.postValue(readOnly) if (readOnly) { - Log.w("$TAG Conversation with subject [${chatRoom.subject}] is read only!") + Log.w("$TAG Conversation with subject [${chatRoom.subjectUtf8}] is read only!") } checkIfConversationShouldBeDisabledForSecurityReasons() - subject.postValue(chatRoom.subject) + subject.postValue(chatRoom.subjectUtf8) computeParticipantsInfo() @@ -693,7 +693,7 @@ class ConversationViewModel val avatar = if (LinphoneUtils.isChatRoomAGroup(chatRoom)) { val fakeFriend = coreContext.core.createFriend() - fakeFriend.name = chatRoom.subject + fakeFriend.name = chatRoom.subjectUtf8 val model = ContactAvatarModel(fakeFriend) model.updateSecurityLevelUsingConversation(chatRoom) model diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationsListViewModel.kt index b1843af655..63879a2e0a 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationsListViewModel.kt @@ -85,7 +85,7 @@ class ConversationsListViewModel if (state == ChatRoom.State.Instantiated) return val id = LinphoneUtils.getConversationId(chatRoom) - Log.i("$TAG Conversation [$id] (${chatRoom.subject}) state changed: [$state]") + Log.i("$TAG Conversation [$id] (${chatRoom.subjectUtf8}) state changed: [$state]") if (state == ChatRoom.State.Created) { Log.i("$TAG Conversation [$id] successfully created") diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/StartConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/StartConversationViewModel.kt index 42cddc41d3..3df35525b9 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/StartConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/StartConversationViewModel.kt @@ -63,7 +63,7 @@ class StartConversationViewModel if (state == ChatRoom.State.Instantiated) return val id = LinphoneUtils.getConversationId(chatRoom) - Log.i("$TAG Conversation [$id] (${chatRoom.subject}) state changed: [$state]") + Log.i("$TAG Conversation [$id] (${chatRoom.subjectUtf8}) state changed: [$state]") if (state == ChatRoom.State.Created) { Log.i("$TAG Conversation [$id] successfully created") diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt index 2aab2ea51b..eb15c30bdb 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt @@ -201,7 +201,7 @@ class ContactViewModel if (state == ChatRoom.State.Instantiated) return val id = LinphoneUtils.getConversationId(chatRoom) - Log.i("$TAG Conversation [$id] (${chatRoom.subject}) state changed: [$state]") + Log.i("$TAG Conversation [$id] (${chatRoom.subjectUtf8}) state changed: [$state]") if (state == ChatRoom.State.Created) { Log.i("$TAG Conversation [$id] successfully created") diff --git a/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt b/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt index ed81b343d2..9eae4870ae 100644 --- a/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryViewModel.kt @@ -110,7 +110,7 @@ class HistoryViewModel if (state == ChatRoom.State.Instantiated) return val id = LinphoneUtils.getConversationId(chatRoom) - Log.i("$TAG Conversation [$id] (${chatRoom.subject}) state changed: [$state]") + Log.i("$TAG Conversation [$id] (${chatRoom.subjectUtf8}) state changed: [$state]") if (state == ChatRoom.State.Created) { Log.i("$TAG Conversation [$id] successfully created") diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt index a4a414a030..053d198c98 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt @@ -448,7 +448,7 @@ abstract class AddressSelectionViewModel null } } else { - if (chatRoom.subject.orEmpty().contains(filter, ignoreCase = true)) { + if (chatRoom.subjectUtf8.orEmpty().contains(filter, ignoreCase = true)) { chatRoom } else { chatRoom.participants.find { @@ -491,7 +491,7 @@ abstract class AddressSelectionViewModel val subject = if (isOneToOne) { friend?.name } else { - chatRoom.subject + chatRoom.subjectUtf8 } val model = ConversationContactOrSuggestionModel( remoteAddress, @@ -502,7 +502,7 @@ abstract class AddressSelectionViewModel val avatarModel = if (!isOneToOne) { val fakeFriend = coreContext.core.createFriend() - fakeFriend.name = chatRoom.subject + fakeFriend.name = chatRoom.subjectUtf8 val avatarModel = ContactAvatarModel(fakeFriend) avatarModel.defaultToConversationIcon.postValue(true) avatarModel diff --git a/app/src/main/java/org/linphone/utils/ShortcutUtils.kt b/app/src/main/java/org/linphone/utils/ShortcutUtils.kt index 6e75f08b9b..2be768383e 100644 --- a/app/src/main/java/org/linphone/utils/ShortcutUtils.kt +++ b/app/src/main/java/org/linphone/utils/ShortcutUtils.kt @@ -140,7 +140,7 @@ class ShortcutUtils { ).buildIcon() } else { isGroup = true - subject = chatRoom.subject.orEmpty() + subject = chatRoom.subjectUtf8.orEmpty() AvatarGenerator(context).setInitials(AppUtils.getInitials(subject)).buildIcon() } From 59435c361468fd96cfff6ffe4590895363e2e898 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 13 Feb 2026 15:23:34 +0100 Subject: [PATCH 449/593] Fixed call log history not updated when deleting items from details fragment --- .../linphone/ui/main/history/fragment/HistoryListFragment.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt index bd7b9f70a4..1c498cebca 100644 --- a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt @@ -267,7 +267,8 @@ class HistoryListFragment : AbstractMainFragment() { sharedViewModel.forceRefreshCallLogsListEvent.observe(viewLifecycleOwner) { it.consume { - listViewModel.applyFilter() + Log.i("$TAG Re-compute call log history") + listViewModel.filter() } } From 8c0499a1efd1022751280c7ee635bc4b3035b3ee Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 16 Feb 2026 10:28:52 +0100 Subject: [PATCH 450/593] Bumped AGP to 9.0.1 & gradle to 9.2.1 --- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 97815a2afd..77c5e02204 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "9.0.0" +agp = "9.0.1" kotlin = "2.3.10" gmsGoogleServices = "4.4.4" firebaseCrashlytics = "3.0.6" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8c88e2b832..dd20b755c9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Jun 22 12:11:25 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 2a815972ff0a9728574fa069e8180713d845146f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 16 Feb 2026 10:56:44 +0100 Subject: [PATCH 451/593] Added developper setting button to delete orphan auth info --- .../settings/viewmodel/SettingsViewModel.kt | 30 ++++++++++++++ .../layout/settings_developer_fragment.xml | 39 ++++++++++++++++--- app/src/main/res/values-fr/strings.xml | 6 +++ app/src/main/res/values/strings.xml | 6 +++ 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index b7a43fbd39..658d7ba148 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -1235,4 +1235,34 @@ class SettingsViewModel showGreenToast(R.string.settings_developer_cleared_native_friends_in_database_toast, R.drawable.trash_simple) } } + + @UiThread + fun clearOrphanAuthInfo() { + coreContext.postOnCoreThread { core -> + var count = 0 + for (authInfo in core.authInfoList) { + val username = authInfo.username + if (username == null) { + Log.i("$TAG Removing auth info [$authInfo] without username") + core.removeAuthInfo(authInfo) + count += 1 + } else { + val account = core.accountList.find { + it.params.identityAddress?.username == username + } + if (account == null) { + Log.i("$TAG Removing auth info [$authInfo] with username [$username] for which no account was found") + core.removeAuthInfo(authInfo) + count += 1 + } + } + } + if (count == 0) { + showGreenToast(R.string.settings_developer_no_auth_info_removed_toast, R.drawable.trash_simple) + } else { + val message = AppUtils.getStringWithPlural(R.plurals.settings_developer_cleared_auth_info_toast, count, "$count") + showFormattedGreenToast(message, R.drawable.warning_circle) + } + } + } } diff --git a/app/src/main/res/layout/settings_developer_fragment.xml b/app/src/main/res/layout/settings_developer_fragment.xml index 694b1ecd60..6ce1ae0cf2 100644 --- a/app/src/main/res/layout/settings_developer_fragment.xml +++ b/app/src/main/res/layout/settings_developer_fragment.xml @@ -315,21 +315,25 @@ app:layout_constraintEnd_toEndOf="parent"/> + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 527a72232e..6227374796 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -332,6 +332,12 @@ Supprimer les contacts natifs importés Ils seront synchronisés à nouveau au prochain démarrage de l\'application sauf si vous retirez la permission de lire les contacts Contacts importés supprimés + Supprimer les informations d\'authentification orphelines + Aucune information d\'authentification orpheline trouvée + + %s information d\'authentification supprimée + %s informations d\'authentification supprimées + Mon compte diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 205559593c..748021857a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -374,6 +374,12 @@ Clear imported contacts from native address book They will be imported again the next time the app starts unless you remove the contacts permission Imported contacts have been deleted + Clear authentication info no longer associated to any account + No orphan authentication info found + + %s orphan authentication info removed + %s orphans authentication info removed + Manage account From 4ac0b789b7bdf662d6e427f89dc52a09465759eb Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 16 Feb 2026 11:32:22 +0100 Subject: [PATCH 452/593] Hide operation in progress label & spinner if SSO fails --- .../sso/viewmodel/SingleSignOnViewModel.kt | 30 ++++++++++++++----- .../res/layout/single_sign_on_fragment.xml | 6 ++-- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt b/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt index a6cf36d933..554fb2f7fd 100644 --- a/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt @@ -51,14 +51,11 @@ class SingleSignOnViewModel private const val TAG = "[Single Sign On ViewModel]" } - val singleSignOnProcessCompletedEvent = MutableLiveData>() + val operationInProgress = MutableLiveData() - private var clientId: String - private val redirectUri: String - - private var singleSignOnUrl = "" - - private var username: String = "" + val singleSignOnProcessCompletedEvent: MutableLiveData> by lazy { + MutableLiveData() + } val startAuthIntentEvent: MutableLiveData> by lazy { MutableLiveData() @@ -68,11 +65,19 @@ class SingleSignOnViewModel MutableLiveData() } + private var clientId: String + private val redirectUri: String + + private var singleSignOnUrl = "" + + private var username: String = "" + private lateinit var authState: AuthState private lateinit var authService: AuthorizationService init { clientId = corePreferences.singleSignOnClientId + operationInProgress.value = true val openIdCallbackScheme = coreContext.context.getString(R.string.linphone_openid_callback_scheme) redirectUri = "$openIdCallbackScheme:/openidcallback" @@ -121,12 +126,14 @@ class SingleSignOnViewModel } else { Log.e("$TAG Can't perform request token [$ex]") onErrorEvent.postValue(Event(ex?.errorDescription.orEmpty())) + operationInProgress.value = false } } @UiThread private fun singleSignOn() { Log.i("$TAG Fetch from issuer [$singleSignOnUrl]") + operationInProgress.postValue(true) AuthorizationServiceConfiguration.fetchFromIssuer( singleSignOnUrl.toUri(), AuthorizationServiceConfiguration.RetrieveConfigurationCallback { serviceConfiguration, ex -> @@ -137,12 +144,14 @@ class SingleSignOnViewModel onErrorEvent.postValue( Event("Failed to fetch configuration from issuer $singleSignOnUrl") ) + operationInProgress.postValue(false) return@RetrieveConfigurationCallback } if (serviceConfiguration == null) { Log.e("$TAG Service configuration is null!") onErrorEvent.postValue(Event("Service configuration is null")) + operationInProgress.postValue(false) return@RetrieveConfigurationCallback } @@ -178,6 +187,7 @@ class SingleSignOnViewModel @UiThread private fun performRefreshToken() { + operationInProgress.postValue(true) if (::authState.isInitialized) { if (!::authService.isInitialized) { authService = AuthorizationService(coreContext.context) @@ -203,6 +213,7 @@ class SingleSignOnViewModel "$TAG Failed to perform token refresh [$ex], destroying auth_state.json file" ) onErrorEvent.postValue(Event(ex?.errorDescription.orEmpty())) + operationInProgress.postValue(false) viewModelScope.launch { FileUtils.deleteFile(authStateJsonFile.absolutePath) @@ -228,6 +239,7 @@ class SingleSignOnViewModel @UiThread private fun performRequestToken(response: AuthorizationResponse) { + operationInProgress.postValue(true) if (::authService.isInitialized) { Log.i("$TAG Starting perform token request") authService.performTokenRequest( @@ -246,6 +258,7 @@ class SingleSignOnViewModel } else { Log.e("$TAG Failed to perform token request [$ex]") onErrorEvent.postValue(Event(ex?.errorDescription.orEmpty())) + operationInProgress.postValue(false) } } } @@ -265,6 +278,7 @@ class SingleSignOnViewModel } catch (exception: Exception) { Log.e("$TAG Failed to use serialized AuthState [$exception]") onErrorEvent.postValue(Event("Failed to read stored AuthState")) + operationInProgress.postValue(false) } } } else { @@ -294,6 +308,7 @@ class SingleSignOnViewModel @UiThread private fun updateTokenInfo() { Log.i("$TAG Updating token info") + operationInProgress.postValue(true) if (::authState.isInitialized) { if (authState.isAuthorized) { @@ -343,6 +358,7 @@ class SingleSignOnViewModel if (expire == null) { Log.e("$TAG Access token expiration time is null!") onErrorEvent.postValue(Event("Invalid access token expiration time")) + operationInProgress.postValue(false) } else { val accessToken = Factory.instance().createBearerToken(authState.accessToken.orEmpty(), expire / 1000) // Linphone timestamps are in seconds diff --git a/app/src/main/res/layout/single_sign_on_fragment.xml b/app/src/main/res/layout/single_sign_on_fragment.xml index b3c002bfd1..a0a0194af1 100644 --- a/app/src/main/res/layout/single_sign_on_fragment.xml +++ b/app/src/main/res/layout/single_sign_on_fragment.xml @@ -24,7 +24,7 @@ android:layout_width="@dimen/top_bar_height" android:layout_height="@dimen/top_bar_height" android:src="@drawable/caret_left" - android:visibility="invisible" + android:visibility="@{viewModel.operationInProgress ? View.INVISIBLE : View.VISIBLE, default=invisible}" android:contentDescription="@string/content_description_go_back_icon" app:tint="?attr/color_main2_500" app:layout_constraintTop_toTopOf="parent" @@ -47,8 +47,9 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" - app:indicatorColor="?attr/color_main1_500" android:indeterminate="true" + android:visibility="@{viewModel.operationInProgress ? View.VISIBLE : View.GONE}" + app:indicatorColor="?attr/color_main1_500" app:layout_constraintVertical_chainStyle="packed" app:layout_constraintTop_toBottomOf="@id/title" app:layout_constraintStart_toStartOf="parent" @@ -66,6 +67,7 @@ android:textSize="18sp" android:layout_below="@id/progress" android:layout_centerHorizontal="true" + android:visibility="@{viewModel.operationInProgress ? View.VISIBLE : View.GONE}" app:layout_constraintTop_toBottomOf="@id/progress" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" From c8e34a33b064337e3d4e440321142a8fc5d3f877 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 16 Feb 2026 11:25:58 +0000 Subject: [PATCH 453/593] Updated translations from Weblate --- app/src/main/res/values-cs/strings.xml | 56 +++++++++++++++++++++++++- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-es/strings.xml | 25 +++++++----- app/src/main/res/values-it/strings.xml | 2 + 4 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 app/src/main/res/values-it/strings.xml diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 50a6393f27..0d4fb36152 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -164,7 +164,7 @@ Oznámení o příchozích hovorech &appName; Oznámení o zmeškaných hovorech &appName; Oznámení služby &appName; - Oznámení o zprávách (&appName;) + Oznámení o okamžitých zprávách Reakce uživatele %1$s na: %2$s Označit jako přečtené Odpovědět @@ -380,7 +380,7 @@ Povolit push notifikace Push notifikace nejsou dostupné! Šifrování zpráv je povinné - URL adresa SIP proxy serveru + Registrační URI Odchozí proxy server Nastavení zásad NAT URL adresa serveru STUN/TURN @@ -851,4 +851,56 @@ Účastníci Smazat importované kontakty z adresáře zařízení Upravit + Rozumím + Nerušit + Oznámení o chybách účtu + Zapnout reproduktor + Schůzka bude zrušena + Chcete odeslat upozornění všem účastníkům? + Vypnout reproduktor + Registrace účtu %s se nezdařila! + Otevřete &appName; pro obnovení registrace + Algoritmus (algoritmy) LIME (oddělené čárkou) + Povolené hodnoty jsou: c25519, c448, c25519k512, c25519mlk512 a c448mlk1024 + Seřadit kontakty podle + Použijte editor kontaktů aplikace &appName; pro nativní kontakty + Skrýt kontakty bez SIP adresy a telefonního čísla + Tento adresář je pouze pro čtení + Přidat protokoly LDAP do protokolů &appName; + Všechna pole musí být vyplněna + Pokročilá nastavení hovorů + Early-media + Automatická odpověď + Automatická odpověď s videem povolená v obou směrech + Klikněte ještě dvakrát, abyste povolili nastavení pro vývojáře + Klikněte ještě jednou pro povolení nastavení pro vývojáře + Povolit měřiče hlasitosti nahrávání/přehrávání během hovoru + Zobrazit pokročilé statistiky hovorů + Seznam domén kompatibilních s push notifikacemi (oddělené čárkami) + Při příštím spuštění aplikace budou znovu importovány, pokud neodstraníte oprávnění pro kontakty + Importované kontakty byly smazány + Odchozí SIP proxy + Pokud je toto pole vyplněno, odchozí proxy bude automaticky povolena. Nechte jej prázdné, chcete-li ji zakázat. + Změnit účet + Bylo dosaženo limitu výsledků vyhledávání, upřesněte své vyhledávání. + Zprávy nejsou šifrovány koncovým šifrováním, proto nesdílejte citlivé informace! + Nešifrovaná konverzace + Zprávy vyměněné v této konverzaci mohou být zachyceny a přečteny jinými osobami než vaším příjemcem, důvěrnost není zaručena! + Tento PDF soubor nelze otevřít, může být poškozen + Zpráva se upravuje + Upraveno + Smazat tuto zprávu? + Pro mě + Pro všechny + Tato zpráva bude smazána + Tuto zprávu jste smazali + Nenalezeni žádní účastníci + Odmítnout + Odpověď + Mikrofon + HDMI + ICE: %s + Rozsah IP: %s + Zrušit úpravu zprávy + Zavřít menu diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3770791fc0..8a3cae12ec 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -305,7 +305,7 @@ Kein Vorschlag und im Moment kein Kontakt… Betreff für Gruppenanruf festlegen Betreff des Gruppenanrufs - Im Moment kein Anruf… + Kein Anruf von/zu diesem Konto gefunden… Chat Möchten Sie wirklich den gesamten Anrufverlauf löschen? Alle Anrufe werden aus dem Verlauf gelöscht diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 4a0d25af11..66e17cf964 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -66,7 +66,7 @@ El archivo se ha exportado a la galería nativa Error tratando de exportar el archivo a la galería nativa El archivo ha sido exportado a documentos - El volumen de los medios es bajo, ¡es posible que no escuches nada! + El volumen de los medios es bajo !es posible que no escuches nada! Configuración aplicada satisfactoriamente Error al intentar descargar y aplicar la configuración remota Términos generales y política de privacidad @@ -76,20 +76,20 @@ Confirmar número de teléfono Acceso Escanear código QR - Código QR no válido! + ¡Código QR no válido! Utilice una cuenta SIP de terceros Cuenta SIP de terceros Inicio de sesión único - La dirección SIP no es válida! + ¡La dirección SIP no es válida! La cuenta ya existe - Aún no tienes cuenta? + ¿Aún no tienes cuenta? Registrar Confirma tu número de teléfono Una aplicación gratuita y de código abierto desde 2001. Error tratando de exportar archivo a documentos Error al intentar crear un reproductor multimedia ¿Estás seguro que tu número de teléfono es %s? - La dirección SIP no contiene un nombre de usuario! + ¡La dirección SIP no contiene un nombre de usuario! Hemos enviado un código de verificación a tu número de teléfono %1$s.\n\nIntroduce el código de verificación a continuación: %s archivo en proceso de carga @@ -102,10 +102,10 @@ %s Archivos en descarga %s Archivo en descarga - Número equivocado? + ¿Número equivocado? Crear Crea una cuenta con tu correo electrónico en: - Ya tienes una cuenta? + ¿Ya tienes una cuenta? Transporte Algunas funciones requieren una cuenta &appName; como la mensajería grupal, las videoconferencias, etc.\n\nEstas funciones están ocultas al registrarse con una cuenta SIP de terceros.\n\nPara habilitarlas en un proyecto comercial, contáctenos. Prefiero crear una cuenta; &appName; @@ -172,7 +172,7 @@ Ajustes Seguridad Cifrar todo - Advertencia: una vez habilitado, ¡no se puede deshabilitar! + Advertencia: una vez habilitado ¡no se puede deshabilitar! ¡Error al habilitar el módulo de cifrado! Módulo de cifrado habilitado ¿Realmente quieres cifrarlo todo? @@ -209,10 +209,10 @@ Dominio de autenticación Almacenar los contactos recién creados La sincronización fue exitosa - Error de sincronización! + ¡Error de sincronización! Cuenta CardDAV eliminada Identificador del dispositivo - Se produjo un error, ¡el servidor LDAP no se guardó! + Se produjo un error ¡el servidor LDAP no se guardó! Respuesta automática de las llamadas entrantes Interfaz de usuario Utilice dos servidores @@ -307,4 +307,9 @@ Conversaciones Contactos Borrar contactos importados de la libreta de direcciones nativa + Buscar + Entendido + No cancelar + ¡El registro de la cuenta %s falló! + Buscar diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml new file mode 100644 index 0000000000..a6b3daec93 --- /dev/null +++ b/app/src/main/res/values-it/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From de3280f0e4b3c6ca07b93fb9535ed64e3b7d4dbc Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 16 Feb 2026 12:23:12 +0100 Subject: [PATCH 454/593] Bumped version code for next beta --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 373b3ba633..0394ddbc12 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -106,7 +106,7 @@ android { applicationId = packageName minSdk = 28 targetSdk = 36 - versionCode = 601004 // 6.01.004 + versionCode = 601005 // 6.01.005 versionName = "6.2.0-beta" manifestPlaceholders["appAuthRedirectScheme"] = packageName From 64f04e4f532597cb8e17df80d4d9b4b6f0fc5504 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 16 Feb 2026 15:03:33 +0100 Subject: [PATCH 455/593] Improved account profile page with section header & connection in progress indicator --- .../viewmodel/AccountProfileViewModel.kt | 1 + .../res/layout/account_profile_fragment.xml | 35 ++++++++++++++++--- .../main/res/layout/meeting_edit_fragment.xml | 2 ++ .../res/layout/meeting_schedule_fragment.xml | 2 ++ app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 38 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt index 72c55e5bf6..84fd96677c 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt @@ -353,6 +353,7 @@ class AccountProfileViewModel registerEnabled.postValue(account.params.isRegisterEnabled) if (!core.isNetworkReachable) { + Log.w("$TAG Network is not reachable, updating registration state to reflect that") // To reflect the difference between Disabled & Disconnected accountModel.value?.updateRegistrationState() } diff --git a/app/src/main/res/layout/account_profile_fragment.xml b/app/src/main/res/layout/account_profile_fragment.xml index 2b9934328e..497649d3a9 100644 --- a/app/src/main/res/layout/account_profile_fragment.xml +++ b/app/src/main/res/layout/account_profile_fragment.xml @@ -5,6 +5,7 @@ + @@ -376,22 +377,35 @@ + + + app:layout_constraintTop_toBottomOf="@id/registration_status"> + + diff --git a/app/src/main/res/layout/meeting_schedule_fragment.xml b/app/src/main/res/layout/meeting_schedule_fragment.xml index 2fe6d82a21..42e987fc0a 100644 --- a/app/src/main/res/layout/meeting_schedule_fragment.xml +++ b/app/src/main/res/layout/meeting_schedule_fragment.xml @@ -327,6 +327,8 @@ android:spinnerMode="dropdown" android:popupBackground="@drawable/shape_squircle_white_background" android:background="@drawable/edit_text_background" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintWidth_max="@dimen/text_input_max_width" app:layout_constraintTop_toBottomOf="@id/timezone_label" app:layout_constraintStart_toStartOf="@id/timezone_label" app:layout_constraintEnd_toEndOf="parent" /> diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6227374796..2cb85ed933 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -347,6 +347,7 @@ Ajouter une image Modifier Supprimer + État de la connexion Vous êtes en ligne, on peut vous joindre. Vous êtes hors ligne, probablement car vous n\'êtes pas actuellement connecté à internet. Compte désactivé, vous ne recevrez ni appel ni message. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 748021857a..195b0565a3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -389,6 +389,7 @@ Add a picture Edit picture Remove picture + Connection status This account in online, everybody can call you. This account in offline, probably because you aren\'t connected to internet right now. Account has been disabled, you won\'t receive any call or message. From 820eec9cf583a80ecd9b534704be2b949c67f8bd Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 16 Feb 2026 15:35:38 +0100 Subject: [PATCH 456/593] Also show print logs in logcat setting in troubleshooting fragment --- .../ui/main/help/viewmodel/HelpViewModel.kt | 19 ++++++++++ .../layout/account_nat_policy_settings.xml | 6 +++ .../res/layout/account_settings_fragment.xml | 4 ++ .../main/res/layout/help_debug_fragment.xml | 37 ++++++++++++++++++- .../layout/settings_developer_fragment.xml | 4 ++ 5 files changed, 69 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/main/help/viewmodel/HelpViewModel.kt b/app/src/main/java/org/linphone/ui/main/help/viewmodel/HelpViewModel.kt index c58e4cfa19..2bb288448e 100644 --- a/app/src/main/java/org/linphone/ui/main/help/viewmodel/HelpViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/help/viewmodel/HelpViewModel.kt @@ -31,6 +31,7 @@ import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.core.Core import org.linphone.core.CoreListenerStub +import org.linphone.core.Factory import org.linphone.core.VersionUpdateCheckResult import org.linphone.core.tools.Log import org.linphone.ui.GenericViewModel @@ -61,6 +62,10 @@ class HelpViewModel val logsUploadInProgress = MutableLiveData() + val printLogsInLogcat = MutableLiveData() + + val developerSettingsEnabled = MutableLiveData() + val canConfigFileBeViewed = MutableLiveData() val newVersionAvailableEvent: MutableLiveData>> by lazy { @@ -165,6 +170,8 @@ class HelpViewModel coreContext.postOnCoreThread { core -> core.addListener(coreListener) + printLogsInLogcat.postValue(corePreferences.printLogsInLogcat) + developerSettingsEnabled.postValue(corePreferences.showDeveloperSettings) checkUpdateAvailable.postValue(corePreferences.checkForUpdateServerUrl.isNotEmpty()) uploadLogsAvailable.postValue(!core.logCollectionUploadServerUrl.isNullOrEmpty()) } @@ -195,6 +202,7 @@ class HelpViewModel coreContext.postOnCoreThread { Log.w("$TAG Enabling developer settings") corePreferences.showDeveloperSettings = true + developerSettingsEnabled.postValue(true) } } NUMBER_OF_CLICK_TO_ENABLE_DEVELOPER_MODE + 1 -> { @@ -231,6 +239,17 @@ class HelpViewModel } } + @UiThread + fun toggleLogcat() { + val newValue = printLogsInLogcat.value == false + coreContext.postOnCoreThread { + corePreferences.printLogsInLogcat = newValue + coreContext.updateLogcatEnabledSetting(newValue) + Factory.instance().enableLogcatLogs(newValue) + printLogsInLogcat.postValue(newValue) + } + } + @UiThread fun showConfigFile() { coreContext.postOnCoreThread { core -> diff --git a/app/src/main/res/layout/account_nat_policy_settings.xml b/app/src/main/res/layout/account_nat_policy_settings.xml index 6d65b71964..21c0946d4d 100644 --- a/app/src/main/res/layout/account_nat_policy_settings.xml +++ b/app/src/main/res/layout/account_nat_policy_settings.xml @@ -45,6 +45,8 @@ android:text="@={viewModel.stunServer}" android:inputType="text|textUri" android:hint="@string/account_settings_stun_server_url_title" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintWidth_max="@dimen/text_input_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/stun_server_title"/> @@ -133,6 +135,8 @@ android:text="@={viewModel.turnUsername}" android:inputType="text|textPersonName" android:hint="@string/account_settings_turn_username_title" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintWidth_max="@dimen/text_input_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/turn_username_title" /> @@ -167,6 +171,8 @@ android:text="@={viewModel.turnPassword}" android:hint="@string/account_settings_turn_password_title" android:inputType="@{viewModel.showTurnPassword ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD : InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD, default=textPassword}" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintWidth_max="@dimen/text_input_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/turn_password_title" diff --git a/app/src/main/res/layout/account_settings_fragment.xml b/app/src/main/res/layout/account_settings_fragment.xml index bc156a025e..b06bb7929a 100644 --- a/app/src/main/res/layout/account_settings_fragment.xml +++ b/app/src/main/res/layout/account_settings_fragment.xml @@ -165,6 +165,8 @@ android:text="@={viewModel.voicemailUri}" android:inputType="text|textUri" android:hint="@string/account_settings_voicemail_uri_title" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintWidth_max="@dimen/text_input_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/voicemail_uri_title" /> @@ -198,6 +200,8 @@ android:text="@={viewModel.mwiUri}" android:inputType="text|textUri" android:hint="@string/account_settings_mwi_uri_title" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintWidth_max="@dimen/text_input_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/mwi_uri_title" diff --git a/app/src/main/res/layout/help_debug_fragment.xml b/app/src/main/res/layout/help_debug_fragment.xml index 0332e83ca0..bf6b56f03b 100644 --- a/app/src/main/res/layout/help_debug_fragment.xml +++ b/app/src/main/res/layout/help_debug_fragment.xml @@ -129,6 +129,41 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"> + + + + + + diff --git a/app/src/main/res/layout/settings_developer_fragment.xml b/app/src/main/res/layout/settings_developer_fragment.xml index 6ce1ae0cf2..f4c9290fa8 100644 --- a/app/src/main/res/layout/settings_developer_fragment.xml +++ b/app/src/main/res/layout/settings_developer_fragment.xml @@ -332,6 +332,8 @@ android:text="@string/settings_developer_clear_native_friends_in_database_title" android:maxLines="2" android:ellipsize="end" + app:layout_constraintWidth_max="@dimen/button_max_width" + app:layout_constraintHorizontal_bias="0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/push_compatible_domains_list"/> @@ -371,6 +373,8 @@ android:text="@string/settings_developer_clear_orphan_auth_info_title" android:maxLines="2" android:ellipsize="end" + app:layout_constraintWidth_max="@dimen/button_max_width" + app:layout_constraintHorizontal_bias="0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/clear_friends_db_subtitle" From 9ecd63ec5fdf106ab114136ff8c0befd7f3ae833 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 16 Feb 2026 16:07:34 +0100 Subject: [PATCH 457/593] Trying to prevent ringtone picker not opening + show error toast if really not available --- .../ui/main/settings/fragment/SettingsFragment.kt | 10 +++++++--- .../ui/main/settings/viewmodel/SettingsViewModel.kt | 7 ++++--- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt index 90c3fbb65b..e862a7602b 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt @@ -36,6 +36,7 @@ import org.linphone.R import org.linphone.compatibility.Compatibility import org.linphone.core.tools.Log import org.linphone.databinding.SettingsFragmentBinding +import org.linphone.ui.GenericActivity import org.linphone.ui.main.fragment.GenericMainFragment import org.linphone.utils.ConfirmationDialogModel import org.linphone.ui.main.settings.viewmodel.SettingsViewModel @@ -176,7 +177,7 @@ class SettingsFragment : GenericMainFragment() { } } - viewModel.goToIncomingCallNotificationChannelSettingsEvent.observe(viewLifecycleOwner) { + viewModel.showRingtonePickerEvent.observe(viewLifecycleOwner) { it.consume { currentRingtone -> try { val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { @@ -184,13 +185,16 @@ class SettingsFragment : GenericMainFragment() { RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_RINGTONE ) - putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, currentRingtone) + if (currentRingtone != null) { + putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, currentRingtone) + } putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, AppUtils.getString(R.string.settings_calls_change_ringtone_pick_title)) } startActivityForResult(intent, RINGTONE_PICKER_INTENT_ID) } catch (e: Exception) { Log.e("$TAG Failed start ringtone picker: $e") - // TODO: show error to user + val toastMessage = getString(R.string.settings_calls_change_ringtone_picker_unavailable_toast) + (requireActivity() as GenericActivity).showRedToast(toastMessage, R.drawable.warning_circle) } } } diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 658d7ba148..affdf62981 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -92,7 +92,7 @@ class SettingsViewModel val autoRecordCalls = MutableLiveData() - val goToIncomingCallNotificationChannelSettingsEvent = MutableLiveData>() + val showRingtonePickerEvent = MutableLiveData>() // Conversations settings val showConversationsSettings = MutableLiveData() @@ -531,9 +531,10 @@ class SettingsViewModel val coreRingtone = core.ring?.toUri() Log.i("$TAG Currently set ringtone in Core is [$coreRingtone], device default ringtone is [$defaultDeviceRingtone]") val currentRingtone = coreRingtone ?: defaultDeviceRingtone - goToIncomingCallNotificationChannelSettingsEvent.postValue(Event(currentRingtone)) + showRingtonePickerEvent.postValue(Event(currentRingtone)) } catch (e: Exception) { - Log.e("$TAG Failed to get current ringtone: $e") + Log.e("$TAG Failed to get current ringtone, opening picker anyway: $e") + showRingtonePickerEvent.postValue(Event(null)) } } } diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2cb85ed933..9cb66ced28 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -220,6 +220,7 @@ Enregistrement automatique des appels Changer de sonnerie Choisissez la sonnerie + Le sélectionneur de sonnerie n\'est pas disponible ! Conversations Télécharger automatiquement les fichiers Rendre visible dans la galerie les médias téléchargés diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 195b0565a3..4fa51ca51f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -262,6 +262,7 @@ Automatically start recording calls Change ringtone Pick ringtone + Ringtone picker isn\'t available! Conversations Auto-download files Make downloaded media public From 93202fe4a5ae943190cd6349f61807f8eb6b820b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 17 Feb 2026 11:47:18 +0100 Subject: [PATCH 458/593] Reworked layout to improve coherence & display on large screen in landscape --- .../ConferenceParticipantsListFragment.kt | 2 + .../ui/call/fragment/CallsListFragment.kt | 2 + .../ui/call/fragment/GenericCallFragment.kt | 11 + .../fragment/RecordingsListFragment.kt | 12 + .../res/layout-land/call_ended_fragment.xml | 2 +- .../res/layout-land/chat_list_fragment.xml | 15 +- .../contact_new_or_edit_fragment.xml | 628 +++++----- .../res/layout-land/history_list_fragment.xml | 15 +- .../layout-land/meetings_list_fragment.xml | 11 - .../account_profile_device_list_cell.xml | 2 +- .../res/layout/account_profile_fragment.xml | 23 +- .../res/layout/account_settings_fragment.xml | 48 +- .../res/layout/address_selected_list_cell.xml | 3 +- .../call_conference_participant_list_cell.xml | 8 +- ..._conference_participants_list_fragment.xml | 42 +- .../main/res/layout/call_ended_fragment.xml | 2 +- .../res/layout/call_transfer_fragment.xml | 307 ++--- .../main/res/layout/calls_list_fragment.xml | 44 +- .../res/layout/chat_conversation_fragment.xml | 10 - .../main/res/layout/chat_info_fragment.xml | 106 +- .../layout/chat_message_forward_fragment.xml | 125 +- app/src/main/res/layout/contact_fragment.xml | 396 +++---- .../res/layout/contact_new_or_edit_cell.xml | 2 +- .../layout/contact_new_or_edit_fragment.xml | 653 ++++++----- .../contact_number_address_list_cell.xml | 3 +- app/src/main/res/layout/drawer_menu.xml | 435 +++---- .../file_media_viewer_child_fragment.xml | 6 + .../generic_add_participants_fragment.xml | 248 ++-- .../main/res/layout/help_debug_fragment.xml | 11 +- app/src/main/res/layout/help_fragment.xml | 8 +- app/src/main/res/layout/history_fragment.xml | 7 +- app/src/main/res/layout/main_activity.xml | 1 - .../main/res/layout/meeting_edit_fragment.xml | 675 +++++------ .../res/layout/meeting_schedule_fragment.xml | 945 +++++++-------- .../res/layout/recording_player_fragment.xml | 6 + .../res/layout/recordings_list_fragment.xml | 109 +- .../settings_advanced_calls_fragment.xml | 31 +- .../res/layout/settings_advanced_fragment.xml | 10 +- .../res/layout/settings_contacts_carddav.xml | 467 ++++---- .../res/layout/settings_contacts_ldap.xml | 1043 +++++++++-------- .../layout/settings_developer_fragment.xml | 3 +- app/src/main/res/layout/settings_fragment.xml | 68 +- .../main/res/layout/start_call_fragment.xml | 417 +++---- .../main/res/layout/start_chat_fragment.xml | 389 +++--- app/src/main/res/values-land/dimen.xml | 2 + app/src/main/res/values-sw600dp/dimen.xml | 1 + app/src/main/res/values/dimen.xml | 1 + 47 files changed, 3866 insertions(+), 3489 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/call/conference/fragment/ConferenceParticipantsListFragment.kt b/app/src/main/java/org/linphone/ui/call/conference/fragment/ConferenceParticipantsListFragment.kt index b0e816f9b0..c444569b70 100644 --- a/app/src/main/java/org/linphone/ui/call/conference/fragment/ConferenceParticipantsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/conference/fragment/ConferenceParticipantsListFragment.kt @@ -94,6 +94,8 @@ class ConferenceParticipantsListFragment : GenericCallFragment() { binding.participantsList.setHasFixedSize(true) binding.participantsList.layoutManager = LinearLayoutManager(requireContext()) + binding.participantsList.outlineProvider = outlineProvider + binding.participantsList.clipToOutline = true binding.setBackClickListener { findNavController().popBackStack() diff --git a/app/src/main/java/org/linphone/ui/call/fragment/CallsListFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/CallsListFragment.kt index 2fd2048ca9..fa8395cde7 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/CallsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/CallsListFragment.kt @@ -85,6 +85,8 @@ class CallsListFragment : GenericCallFragment() { binding.callsList.setHasFixedSize(true) binding.callsList.layoutManager = LinearLayoutManager(requireContext()) + binding.callsList.outlineProvider = outlineProvider + binding.callsList.clipToOutline = true adapter.callLongClickedEvent.observe(viewLifecycleOwner) { it.consume { model -> diff --git a/app/src/main/java/org/linphone/ui/call/fragment/GenericCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/GenericCallFragment.kt index 6d0aded9ff..d759085356 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/GenericCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/GenericCallFragment.kt @@ -21,11 +21,14 @@ package org.linphone.ui.call.fragment import android.annotation.SuppressLint import android.content.res.Configuration +import android.graphics.Outline import android.os.Bundle import android.view.MotionEvent import android.view.View +import android.view.ViewOutlineProvider import androidx.annotation.UiThread import androidx.lifecycle.ViewModelProvider +import org.linphone.R import org.linphone.core.tools.Log import org.linphone.ui.GenericFragment import org.linphone.ui.call.view.RoundCornersTextureView @@ -39,6 +42,14 @@ abstract class GenericCallFragment : GenericFragment() { protected lateinit var sharedViewModel: SharedCallViewModel + protected val outlineProvider = object : ViewOutlineProvider() { + override fun getOutline(view: View?, outline: Outline?) { + val radius = resources.getDimension(R.dimen.top_list_item_rounded_corner_radius) + view ?: return + outline?.setRoundRect(0, 0, view.width, (view.height + radius).toInt(), radius) + } + } + // For moving video preview purposes private val videoPreviewTouchListener = View.OnTouchListener { view, event -> when (event.action) { diff --git a/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsListFragment.kt b/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsListFragment.kt index a152a848d0..25e45fd993 100644 --- a/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsListFragment.kt @@ -21,10 +21,12 @@ package org.linphone.ui.main.recordings.fragment import android.content.ActivityNotFoundException import android.content.Intent +import android.graphics.Outline import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.ViewOutlineProvider import androidx.annotation.UiThread import androidx.core.content.FileProvider import androidx.lifecycle.ViewModelProvider @@ -63,6 +65,14 @@ class RecordingsListFragment : GenericMainFragment() { private var bottomSheetDialog: BottomSheetDialogFragment? = null + private val outlineProvider = object : ViewOutlineProvider() { + override fun getOutline(view: View?, outline: Outline?) { + val radius = resources.getDimension(R.dimen.top_list_item_rounded_corner_radius) + view ?: return + outline?.setRoundRect(0, 0, view.width, (view.height + radius).toInt(), radius) + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -94,6 +104,8 @@ class RecordingsListFragment : GenericMainFragment() { binding.recordingsList.layoutManager = LinearLayoutManager(requireContext()) val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter) binding.recordingsList.addItemDecoration(headerItemDecoration) + binding.recordingsList.outlineProvider = outlineProvider + binding.recordingsList.clipToOutline = true listViewModel.recordings.observe(viewLifecycleOwner) { val count = it.size diff --git a/app/src/main/res/layout-land/call_ended_fragment.xml b/app/src/main/res/layout-land/call_ended_fragment.xml index 0f2012cfa5..bb6b3b2087 100644 --- a/app/src/main/res/layout-land/call_ended_fragment.xml +++ b/app/src/main/res/layout-land/call_ended_fragment.xml @@ -30,7 +30,7 @@ android:id="@+id/call_media_encryption_info" android:layout_width="0dp" android:layout_height="wrap_content" - android:visibility="@{viewModel.callDuration > 0 ? View.VISIBLE : View.GONE}" + android:visibility="@{viewModel.callDuration > 0 ? View.VISIBLE : View.INVISIBLE, default=invisible}" layout="@layout/call_media_encryption_info" bind:viewModel="@{viewModel}" app:layout_constraintTop_toBottomOf="@id/call_direction_label" diff --git a/app/src/main/res/layout-land/chat_list_fragment.xml b/app/src/main/res/layout-land/chat_list_fragment.xml index c67703ca6a..c97ad46c67 100644 --- a/app/src/main/res/layout-land/chat_list_fragment.xml +++ b/app/src/main/res/layout-land/chat_list_fragment.xml @@ -55,17 +55,6 @@ app:layout_constraintStart_toEndOf="@id/bottom_nav_bar" app:layout_constraintEnd_toEndOf="parent"/> - - diff --git a/app/src/main/res/layout-land/contact_new_or_edit_fragment.xml b/app/src/main/res/layout-land/contact_new_or_edit_fragment.xml index 268587e4aa..b569fae36b 100644 --- a/app/src/main/res/layout-land/contact_new_or_edit_fragment.xml +++ b/app/src/main/res/layout-land/contact_new_or_edit_fragment.xml @@ -49,323 +49,365 @@ app:layout_constraintTop_toTopOf="parent" tools:ignore="RtlSymmetry" /> - - - - - - - - - - - - - - + - - - - + android:paddingBottom="@dimen/screen_bottom_margin"> - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + android:background="@drawable/shape_squircle_white_background" + app:layout_constraintWidth_max="@dimen/section_max_width" + app:layout_constraintTop_toBottomOf="@id/avatar_content" + app:layout_constraintStart_toEndOf="@id/left_column" + app:layout_constraintEnd_toEndOf="parent"> + + + + + + + + + + - + - + - - + app:layout_constraintTop_toBottomOf="@id/top_bar" /> diff --git a/app/src/main/res/layout-land/meetings_list_fragment.xml b/app/src/main/res/layout-land/meetings_list_fragment.xml index 187dd63fb5..61f7f0e054 100644 --- a/app/src/main/res/layout-land/meetings_list_fragment.xml +++ b/app/src/main/res/layout-land/meetings_list_fragment.xml @@ -56,17 +56,6 @@ app:layout_constraintStart_toEndOf="@id/bottom_nav_bar" app:layout_constraintEnd_toEndOf="parent"/> - - diff --git a/app/src/main/res/layout/account_profile_fragment.xml b/app/src/main/res/layout/account_profile_fragment.xml index 497649d3a9..de59711c2a 100644 --- a/app/src/main/res/layout/account_profile_fragment.xml +++ b/app/src/main/res/layout/account_profile_fragment.xml @@ -102,6 +102,7 @@ android:layout_marginEnd="16dp" android:layout_marginTop="20dp" android:background="@drawable/shape_squircle_white_background" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -198,6 +199,7 @@ android:drawableEnd="@{viewModel.expandDetails ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" android:background="@drawable/squircle_transparent_button_background" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/avatar_content"/> @@ -210,6 +212,7 @@ android:layout_marginEnd="16dp" android:background="@drawable/shape_squircle_white_background" android:visibility="@{viewModel.expandDetails ? View.VISIBLE : View.GONE}" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/details"> @@ -220,7 +223,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" - android:layout_marginTop="20dp" + android:layout_marginTop="10dp" android:text="@string/sip_address" android:visibility="@{!viewModel.hideSipAddresses || viewModel.showDeviceId ? View.VISIBLE : View.GONE}" app:layout_constraintStart_toStartOf="parent" @@ -309,7 +312,6 @@ android:background="@drawable/edit_text_background" android:inputType="text|textPersonName|textCapSentences" app:layout_constraintHorizontal_bias="0" - app:layout_constraintWidth_max="@dimen/text_input_max_width" app:layout_constraintTop_toBottomOf="@id/display_name_label" app:layout_constraintStart_toStartOf="@id/display_name_label" app:layout_constraintEnd_toEndOf="parent"/> @@ -345,7 +347,7 @@ android:layout_width="0dp" android:layout_height="50dp" android:layout_marginEnd="16dp" - android:layout_marginBottom="20dp" + android:layout_marginBottom="10dp" android:paddingStart="20dp" android:paddingEnd="20dp" android:textSize="14sp" @@ -357,7 +359,6 @@ android:popupBackground="@drawable/shape_squircle_white_background" android:background="@drawable/edit_text_background" app:layout_constraintHorizontal_bias="0" - app:layout_constraintWidth_max="@dimen/text_input_max_width" app:layout_constraintTop_toBottomOf="@id/prefix_label" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="@id/prefix_label" @@ -385,8 +386,9 @@ android:padding="10dp" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:layout_marginTop="20dp" + android:layout_marginTop="10dp" android:text="@string/manage_account_status_registration_status_title" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/details_content"/> @@ -398,6 +400,7 @@ android:layout_marginStart="16dp" android:layout_marginEnd="16dp" android:background="@drawable/shape_squircle_white_background" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/registration_status"> @@ -471,6 +474,7 @@ android:drawableEnd="@{viewModel.expandDevices ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" android:background="@drawable/squircle_transparent_button_background" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/connection_content"/> @@ -480,17 +484,16 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:orientation="vertical" - android:layout_marginBottom="8dp" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:paddingTop="6dp" android:paddingStart="16dp" android:paddingEnd="16dp" - android:paddingBottom="20dp" + android:paddingBottom="10dp" android:background="@drawable/shape_squircle_white_background" android:visibility="@{viewModel.isOnDefaultDomain && viewModel.expandDevices ? View.VISIBLE : View.GONE, default=gone}" app:entries="@{viewModel.devices}" app:layout="@{@layout/account_profile_device_list_cell}" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintHeight_min="80dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" @@ -534,6 +537,7 @@ android:layout_marginEnd="16dp" android:layout_marginTop="20dp" android:text="@string/contact_details_actions_title" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/devices_list"/> @@ -550,6 +554,7 @@ android:text="@string/manage_account_settings" android:drawableStart="@drawable/gear" android:visibility="@{viewModel.hideAccountSettings ? View.GONE : View.VISIBLE}" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintTop_toBottomOf="@id/actions" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> @@ -562,10 +567,10 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:layout_marginBottom="@dimen/screen_bottom_margin" android:background="@drawable/action_background_bottom" android:text="@string/manage_account_delete" android:drawableStart="@drawable/sign_out" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintTop_toBottomOf="@id/action_settings" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"/> diff --git a/app/src/main/res/layout/account_settings_fragment.xml b/app/src/main/res/layout/account_settings_fragment.xml index b06bb7929a..a086b576d0 100644 --- a/app/src/main/res/layout/account_settings_fragment.xml +++ b/app/src/main/res/layout/account_settings_fragment.xml @@ -62,18 +62,23 @@ app:layout_constraintTop_toBottomOf="@id/title" app:layout_constraintBottom_toBottomOf="parent"> - + android:paddingBottom="@dimen/screen_bottom_margin"> + android:background="@drawable/shape_squircle_white_background" + app:layout_constraintWidth_max="@dimen/section_max_width" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:background="@drawable/squircle_transparent_button_background" + app:layout_constraintWidth_max="@dimen/section_max_width" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/account_settings" /> + bind:viewModel="@{viewModel}" + app:layout_constraintWidth_max="@dimen/section_max_width" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/nat_policy_settings_header"/> + android:background="@drawable/squircle_transparent_button_background" + app:layout_constraintWidth_max="@dimen/section_max_width" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/nat_policy_settings" /> + bind:viewModel="@{viewModel}" + app:layout_constraintWidth_max="@dimen/section_max_width" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/advanced_settings_header"/> - + diff --git a/app/src/main/res/layout/address_selected_list_cell.xml b/app/src/main/res/layout/address_selected_list_cell.xml index d7110415f6..92d47191bd 100644 --- a/app/src/main/res/layout/address_selected_list_cell.xml +++ b/app/src/main/res/layout/address_selected_list_cell.xml @@ -13,8 +13,7 @@ + android:padding="5dp"> - + app:layout_constraintEnd_toEndOf="parent"> - + + + + + + + - + app:layout_constraintEnd_toEndOf="parent"> - + - + - + - + - + - + - + - + - + - + - + + + + + + + - + app:layout_constraintEnd_toEndOf="parent"> - + + + + + + + - - @@ -219,6 +220,7 @@ android:layout_marginEnd="16dp" android:layout_marginTop="20dp" android:background="@drawable/shape_squircle_white_background" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintTop_toBottomOf="@id/avatar_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"> @@ -352,61 +354,62 @@ android:text="@{viewModel.participantsLabel, default=@string/conversation_info_participants_list_title}" android:visibility="@{viewModel.isGroup ? View.VISIBLE : View.GONE}" android:background="@drawable/squircle_transparent_button_background" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/call_actions" /> - - - - - + app:layout_constraintEnd_toEndOf="parent"> + + + + + + + app:layout_constraintTop_toBottomOf="@id/participants_list" /> @@ -450,6 +455,7 @@ android:drawableStart="@drawable/file_pdf" android:onClick="@{goToSharedDocumentsClickListener}" android:text="@string/conversation_document_list_title" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/action_media" /> @@ -475,6 +481,7 @@ android:layout_marginEnd="20dp" android:padding="5dp" android:text="@string/contact_details_actions_title" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/action_documents" /> @@ -491,6 +498,7 @@ android:onClick="@{goToContactClickListener}" android:text="@string/conversation_info_menu_go_to_contact" android:visibility="@{!viewModel.isGroup && viewModel.friendAvailable ? View.VISIBLE : View.GONE, default=gone}" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/other_actions" /> @@ -507,6 +515,7 @@ android:onClick="@{addToContactsClickListener}" android:text="@string/conversation_info_menu_add_to_contacts" android:visibility="@{!viewModel.isGroup && !viewModel.friendAvailable && !viewModel.disableAddContact ? View.VISIBLE : View.GONE, default=gone}" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/action_see_contact" /> @@ -523,6 +532,7 @@ android:onClick="@{configureEphemeralMessagesClickListener}" android:text="@string/conversation_action_configure_ephemeral_messages" android:visibility="@{viewModel.isEndToEndEncrypted && !viewModel.isReadOnly ? View.VISIBLE : View.GONE}" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/action_add_to_contacts" /> @@ -539,6 +549,7 @@ android:onClick="@{() -> viewModel.leaveGroup()}" android:text="@string/conversation_action_leave_group" android:visibility="@{viewModel.isGroup && !viewModel.isReadOnly ? View.VISIBLE : View.GONE, default=gone}" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/action_ephemeral_messages" /> @@ -554,6 +565,7 @@ android:drawableStart="@drawable/trash_simple" android:onClick="@{deleteHistoryClickListener}" android:text="@string/conversation_info_delete_history_action" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/action_leave_group" /> diff --git a/app/src/main/res/layout/chat_message_forward_fragment.xml b/app/src/main/res/layout/chat_message_forward_fragment.xml index 7941b6b5e6..b1d2459cc7 100644 --- a/app/src/main/res/layout/chat_message_forward_fragment.xml +++ b/app/src/main/res/layout/chat_message_forward_fragment.xml @@ -47,66 +47,81 @@ app:layout_constraintStart_toEndOf="@id/back" app:layout_constraintTop_toTopOf="parent"/> - + app:layout_constraintEnd_toEndOf="parent"> - + - - - + + + + + + + + + diff --git a/app/src/main/res/layout/contact_fragment.xml b/app/src/main/res/layout/contact_fragment.xml index 603031e901..b856082822 100644 --- a/app/src/main/res/layout/contact_fragment.xml +++ b/app/src/main/res/layout/contact_fragment.xml @@ -86,24 +86,6 @@ android:layout_height="wrap_content" android:paddingBottom="@dimen/screen_bottom_margin"> - - - - - - @@ -165,6 +148,7 @@ android:layout_marginEnd="16dp" android:layout_marginTop="20dp" android:background="@drawable/shape_squircle_white_background" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintTop_toBottomOf="@id/avatar_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"> @@ -281,14 +265,15 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:padding="10dp" - android:layout_marginStart="20dp" - android:layout_marginEnd="20dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" android:layout_marginTop="20dp" android:text="@string/contact_details_numbers_and_addresses_title" android:drawableEnd="@{viewModel.expandNumbersAndAddresses ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.atLeastOneSipAddressOrPhoneNumber ? View.VISIBLE : View.GONE}" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/call_actions"/> @@ -301,84 +286,89 @@ android:orientation="vertical" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:padding="10dp" android:background="@drawable/shape_squircle_white_background" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/numbers_and_addresses_label" app:entries="@{viewModel.sipAddressesAndPhoneNumbers}" app:layout="@{@layout/contact_number_address_list_cell}" /> - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"> - + - + - + - + + + + app:layout_constraintStart_toStartOf="@id/devices_trust" + app:layout_constraintTop_toBottomOf="@id/contact_job_info"/> - - - - - - - - - - - - - - - - + + - - + app:layout_constraintEnd_toEndOf="parent"> + + + + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/devices_trust"/> @@ -576,6 +565,7 @@ android:text="@{viewModel.isFavourite ? @string/contact_details_remove_from_favourites : @string/contact_details_add_to_favourites, default=@string/contact_details_add_to_favourites}" android:drawableStart="@{viewModel.isFavourite ? @drawable/heart_fill : @drawable/heart, default=@drawable/heart_fill}" android:drawableTint="@{viewModel.isFavourite ? @color/danger_500 : @color/main2_500, default=@color/danger_500}" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/action_edit" /> @@ -591,6 +581,7 @@ android:background="@{viewModel.isStored && !viewModel.isReadOnly ? (viewModel.isNative ? @drawable/action_background_bottom : @drawable/action_background_middle) : @drawable/action_background_full, default=@drawable/action_background_middle}" android:text="@string/contact_details_share" android:drawableStart="@drawable/share_network" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/action_favorite"/> @@ -607,6 +598,7 @@ android:text="@string/contact_details_delete" android:drawableStart="@drawable/trash_simple" android:visibility="@{viewModel.isStored && !viewModel.isReadOnly && !viewModel.isNative ? View.VISIBLE : View.GONE}" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/action_share"/> diff --git a/app/src/main/res/layout/contact_new_or_edit_cell.xml b/app/src/main/res/layout/contact_new_or_edit_cell.xml index e63d7874c5..a71f56de05 100644 --- a/app/src/main/res/layout/contact_new_or_edit_cell.xml +++ b/app/src/main/res/layout/contact_new_or_edit_cell.xml @@ -47,7 +47,7 @@ android:src="@drawable/x" android:background="@drawable/squircle_transparent_button_background" android:contentDescription="@string/content_description_contact_remove_field" - app:tint="?attr/color_main2_700" + app:tint="?attr/color_main2_600" app:layout_constraintStart_toEndOf="@id/field" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/field" diff --git a/app/src/main/res/layout/contact_new_or_edit_fragment.xml b/app/src/main/res/layout/contact_new_or_edit_fragment.xml index 80ca11b00f..88dc9bba8b 100644 --- a/app/src/main/res/layout/contact_new_or_edit_fragment.xml +++ b/app/src/main/res/layout/contact_new_or_edit_fragment.xml @@ -49,301 +49,376 @@ app:layout_constraintTop_toTopOf="parent" tools:ignore="RtlSymmetry" /> - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:paddingBottom="@dimen/screen_bottom_margin"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:layout_height="match_parent"> - + app:layout_constraintEnd_toEndOf="parent"> - + - + - + - + - + android:orientation="vertical" + entries="@{viewModel.accounts}" + layout="@{@layout/account_list_cell}"> - + - + - + - + - + - + - + - + - + - + - + + + + + diff --git a/app/src/main/res/layout/file_media_viewer_child_fragment.xml b/app/src/main/res/layout/file_media_viewer_child_fragment.xml index 4af9021f6b..894ebf5868 100644 --- a/app/src/main/res/layout/file_media_viewer_child_fragment.xml +++ b/app/src/main/res/layout/file_media_viewer_child_fragment.xml @@ -75,8 +75,10 @@ android:adjustViewBounds="true" android:contentDescription="@string/content_description_play_pause_video_playback" android:background="@drawable/circle_transparent_dark_button_background" + app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toStartOf="@id/progress" app:tint="@color/bc_white"/> diff --git a/app/src/main/res/layout/generic_add_participants_fragment.xml b/app/src/main/res/layout/generic_add_participants_fragment.xml index 33ec73b5ee..6a4fc69a0a 100644 --- a/app/src/main/res/layout/generic_add_participants_fragment.xml +++ b/app/src/main/res/layout/generic_add_participants_fragment.xml @@ -46,125 +46,145 @@ app:layout_constraintStart_toEndOf="@id/back" app:layout_constraintTop_toTopOf="parent" /> - - - - - + app:layout_constraintEnd_toEndOf="parent"> - - - - - - - - - - - - - + android:layout_marginTop="10dp" + android:layout_marginStart="10dp" + android:text="@{viewModel.selectionCount, default=`0 selected`}" + android:textSize="12sp" + android:textColor="?attr/color_main2_900" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + + + + + + + + + + + + + + + android:layout_height="wrap_content" + android:paddingBottom="@dimen/screen_bottom_margin"> @@ -330,10 +331,8 @@ android:id="@+id/show_config_file" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="16dp" android:layout_marginEnd="16dp" android:layout_marginTop="20dp" - android:layout_marginBottom="@dimen/screen_bottom_margin" android:background="@drawable/tertiary_button_background" android:paddingStart="12dp" android:paddingEnd="12dp" @@ -345,7 +344,7 @@ android:ellipsize="end" android:visibility="@{viewModel.canConfigFileBeViewed ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintVertical_bias="0" - app:layout_constraintStart_toStartOf="parent" + app:layout_constraintStart_toStartOf="@id/info_section" app:layout_constraintTop_toBottomOf="@id/info_section" app:layout_constraintBottom_toBottomOf="parent"/> diff --git a/app/src/main/res/layout/help_fragment.xml b/app/src/main/res/layout/help_fragment.xml index a0284c2e82..589bf8fe87 100644 --- a/app/src/main/res/layout/help_fragment.xml +++ b/app/src/main/res/layout/help_fragment.xml @@ -74,7 +74,8 @@ + android:layout_height="wrap_content" + android:paddingBottom="@dimen/screen_bottom_margin"> @@ -369,6 +372,7 @@ android:layout_marginTop="20dp" android:padding="10dp" android:text="@string/help_about_advanced_title" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintTop_toBottomOf="@id/about_section" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> @@ -379,8 +383,8 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:layout_marginBottom="@dimen/screen_bottom_margin" android:background="@drawable/shape_squircle_white_background" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintVertical_bias="0" app:layout_constraintTop_toBottomOf="@id/advanced_title" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/history_fragment.xml b/app/src/main/res/layout/history_fragment.xml index d58eddba11..a637f497d4 100644 --- a/app/src/main/res/layout/history_fragment.xml +++ b/app/src/main/res/layout/history_fragment.xml @@ -81,7 +81,8 @@ + android:layout_height="wrap_content" + android:paddingBottom="@dimen/screen_bottom_margin"> @@ -168,6 +170,7 @@ android:layout_marginEnd="16dp" android:layout_marginTop="20dp" android:background="@drawable/shape_squircle_white_background" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintTop_toBottomOf="@id/avatar_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"> @@ -344,9 +347,9 @@ android:layout_marginTop="20dp" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:layout_marginBottom="@dimen/screen_bottom_margin" android:background="@drawable/shape_squircle_white_background" android:nestedScrollingEnabled="true" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintVertical_bias="0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index 3f193fca5e..3429395800 100644 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -89,7 +89,6 @@ android:name="org.linphone.ui.main.fragment.DrawerMenuFragment" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?attr/color_main2_000" android:layout_gravity="start" app:layout="@layout/drawer_menu" /> diff --git a/app/src/main/res/layout/meeting_edit_fragment.xml b/app/src/main/res/layout/meeting_edit_fragment.xml index bc87833174..88818606f7 100644 --- a/app/src/main/res/layout/meeting_edit_fragment.xml +++ b/app/src/main/res/layout/meeting_edit_fragment.xml @@ -59,343 +59,354 @@ app:layout_constraintStart_toEndOf="@id/back" app:layout_constraintTop_toTopOf="parent"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - + android:paddingBottom="@dimen/screen_bottom_margin"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/meeting_schedule_fragment.xml b/app/src/main/res/layout/meeting_schedule_fragment.xml index 42e987fc0a..6797243bd8 100644 --- a/app/src/main/res/layout/meeting_schedule_fragment.xml +++ b/app/src/main/res/layout/meeting_schedule_fragment.xml @@ -59,479 +59,490 @@ app:layout_constraintStart_toEndOf="@id/back" app:layout_constraintTop_toTopOf="parent"/> - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:paddingBottom="@dimen/screen_bottom_margin"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/recording_player_fragment.xml b/app/src/main/res/layout/recording_player_fragment.xml index 6ade266808..cc4fc7596e 100644 --- a/app/src/main/res/layout/recording_player_fragment.xml +++ b/app/src/main/res/layout/recording_player_fragment.xml @@ -62,8 +62,10 @@ android:adjustViewBounds="true" android:contentDescription="@string/content_description_play_pause_audio_playback" android:background="@drawable/circle_transparent_dark_button_background" + app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toStartOf="@id/progress" app:tint="@color/bc_white"/> - - - - - - - + app:barrierDirection="bottom" + app:constraint_referenced_ids="back, cancel_search" /> - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"> + + + + + + + + + + + + + android:layout_height="wrap_content" + android:paddingBottom="@dimen/screen_bottom_margin"> @@ -239,6 +241,7 @@ android:drawableEnd="@{viewModel.expandEarlyMedia ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" android:background="@drawable/squircle_transparent_button_background" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/main_section"/> @@ -246,12 +249,15 @@ @@ -275,12 +282,15 @@ @@ -331,23 +343,24 @@ android:drawableEnd="@{viewModel.expandVideoCodecs ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" android:background="@drawable/squircle_transparent_button_background" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/audio_codecs"/> + android:layout_height="wrap_content" + android:paddingBottom="@dimen/screen_bottom_margin"> @@ -261,6 +263,7 @@ android:drawableEnd="@{viewModel.expandAudioDevices ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" android:background="@drawable/squircle_transparent_button_background" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/main_section"/> @@ -275,6 +278,7 @@ android:background="@drawable/shape_squircle_white_background" android:orientation="vertical" android:visibility="@{viewModel.expandAudioDevices ? View.VISIBLE : View.GONE, default=gone}" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintTop_toBottomOf="@id/audio_devices_title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"> @@ -394,7 +398,7 @@ android:drawableEnd="@drawable/arrow_square_out" android:drawableTint="?attr/color_main2_600" android:background="@drawable/squircle_transparent_button_background" - app:layout_constraintHorizontal_bias="1" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintVertical_bias="0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/settings_contacts_carddav.xml b/app/src/main/res/layout/settings_contacts_carddav.xml index 41b98b9be6..d70dc43981 100644 --- a/app/src/main/res/layout/settings_contacts_carddav.xml +++ b/app/src/main/res/layout/settings_contacts_carddav.xml @@ -63,253 +63,264 @@ app:layout_constraintTop_toTopOf="@id/back" app:layout_constraintBottom_toBottomOf="@id/back"/> - + app:layout_constraintBottom_toBottomOf="parent"> + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - + android:paddingBottom="@dimen/screen_bottom_margin"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -90,12 +91,13 @@ @@ -113,6 +115,7 @@ android:drawableEnd="@{viewModel.expandCalls ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableTint="?attr/color_main2_600" android:background="@drawable/squircle_transparent_button_background" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/security_settings"/> @@ -120,12 +123,15 @@ @@ -144,6 +150,7 @@ android:drawableTint="?attr/color_main2_600" android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.showConversationsSettings ? View.VISIBLE : View.GONE}" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/calls_settings"/> @@ -151,12 +158,15 @@ @@ -181,12 +192,15 @@ @@ -211,12 +226,15 @@ @@ -240,12 +259,15 @@ @@ -269,12 +292,15 @@ @@ -299,12 +326,15 @@ @@ -343,6 +374,7 @@ android:drawableTint="?attr/color_main2_600" android:background="@drawable/squircle_transparent_button_background" android:visibility="@{viewModel.showDeveloperSettings ? View.VISIBLE : View.GONE, default=gone}" + app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintVertical_bias="0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/start_call_fragment.xml b/app/src/main/res/layout/start_call_fragment.xml index 484aca5a3b..a6df330cff 100644 --- a/app/src/main/res/layout/start_call_fragment.xml +++ b/app/src/main/res/layout/start_call_fragment.xml @@ -28,13 +28,6 @@ android:layout_height="match_parent" android:background="?attr/color_background_contrast_in_dark_mode"> - - - - - + app:layout_constraintEnd_toEndOf="parent"> - + - + android:layout_marginTop="5dp" + android:layout_marginStart="10dp" + android:layout_marginEnd="10dp" + android:background="@drawable/shape_squircle_white_background" + android:scrollbars="none" + android:visibility="@{viewModel.multipleSelectionMode ? View.VISIBLE : View.GONE, default=gone}" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/multiple_selection_count"> - + - + - + - + - - - + - - + - + - + - + + - + - + + + + + + + + + + + - - - + app:layout_constraintEnd_toEndOf="parent"> - - - + - + android:layout_marginTop="5dp" + android:layout_marginStart="10dp" + android:layout_marginEnd="10dp" + android:background="@drawable/shape_squircle_white_background" + android:scrollbars="none" + android:visibility="@{viewModel.multipleSelectionMode ? View.VISIBLE : View.GONE, default=gone}" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/multiple_selection_count"> - + - + - + - + - + - - + - + - + + - + - + - + + + + + + + + + 110dp 235dp + 600dp 600dp 0dp + 20dp 175dp \ No newline at end of file diff --git a/app/src/main/res/values-sw600dp/dimen.xml b/app/src/main/res/values-sw600dp/dimen.xml index 8710e1796c..86af787802 100644 --- a/app/src/main/res/values-sw600dp/dimen.xml +++ b/app/src/main/res/values-sw600dp/dimen.xml @@ -4,6 +4,7 @@ 600dp 90dp 500dp + 800dp 8 diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 506d7b2f88..ddc4997e77 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -49,6 +49,7 @@ 54dp 55dp 70dp + 400dp 400dp 350dp 400dp From 2dde1c250939104212bf3cc6c283553184cdae7b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 17 Feb 2026 15:00:41 +0100 Subject: [PATCH 459/593] Small improvement over suggestion long press effect --- .../layout/generic_address_picker_contact_list_cell.xml | 4 +++- .../generic_address_picker_conversation_list_cell.xml | 4 +++- .../layout/generic_address_picker_suggestion_list_cell.xml | 7 +++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/generic_address_picker_contact_list_cell.xml b/app/src/main/res/layout/generic_address_picker_contact_list_cell.xml index 9933983bc7..7204cf1161 100644 --- a/app/src/main/res/layout/generic_address_picker_contact_list_cell.xml +++ b/app/src/main/res/layout/generic_address_picker_contact_list_cell.xml @@ -21,7 +21,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="4dp" - android:layout_marginEnd="16dp" + android:layout_marginEnd="4dp" android:paddingTop="5dp" android:paddingBottom="5dp" android:background="@drawable/primary_cell_background"> @@ -60,6 +60,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="10dp" + android:layout_marginEnd="12dp" android:text="@{model.name, default=`John Doe`}" android:textSize="14sp" android:maxLines="1" @@ -74,6 +75,7 @@ android:id="@+id/selected" android:layout_width="@dimen/icon_size" android:layout_height="@dimen/icon_size" + android:layout_marginEnd="12dp" android:contentDescription="@null" android:src="@drawable/check" android:visibility="@{model.selected ? View.VISIBLE : View.GONE, default=gone}" diff --git a/app/src/main/res/layout/generic_address_picker_conversation_list_cell.xml b/app/src/main/res/layout/generic_address_picker_conversation_list_cell.xml index 33ad5ff4d8..232b65ff58 100644 --- a/app/src/main/res/layout/generic_address_picker_conversation_list_cell.xml +++ b/app/src/main/res/layout/generic_address_picker_conversation_list_cell.xml @@ -18,7 +18,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="4dp" - android:layout_marginEnd="16dp" + android:layout_marginEnd="4dp" android:paddingTop="5dp" android:paddingBottom="5dp" android:background="@drawable/primary_cell_background"> @@ -42,6 +42,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="10dp" + android:layout_marginEnd="12dp" android:text="@{model.name, default=`Friends group`}" android:textSize="14sp" android:maxLines="1" @@ -56,6 +57,7 @@ android:id="@+id/selected" android:layout_width="@dimen/icon_size" android:layout_height="@dimen/icon_size" + android:layout_marginEnd="12dp" android:contentDescription="@null" android:src="@drawable/check" android:visibility="@{model.selected ? View.VISIBLE : View.GONE, default=gone}" diff --git a/app/src/main/res/layout/generic_address_picker_suggestion_list_cell.xml b/app/src/main/res/layout/generic_address_picker_suggestion_list_cell.xml index 134c12a918..74fb0e2c9d 100644 --- a/app/src/main/res/layout/generic_address_picker_suggestion_list_cell.xml +++ b/app/src/main/res/layout/generic_address_picker_suggestion_list_cell.xml @@ -17,8 +17,8 @@ android:onClick="@{onClickListener}" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_marginEnd="16dp" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp" android:paddingTop="5dp" android:paddingBottom="5dp" android:background="@drawable/primary_cell_background"> @@ -27,6 +27,7 @@ android:id="@+id/avatar" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_marginStart="12dp" android:layout_marginTop="5dp" android:layout_marginBottom="5dp" layout="@layout/contact_avatar" @@ -41,6 +42,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="10dp" + android:layout_marginEnd="12dp" android:text="@{model.sipUri, default=`john.doe@sip.linphone.org`}" android:textSize="14sp" android:maxLines="1" @@ -55,6 +57,7 @@ android:id="@+id/selected" android:layout_width="@dimen/icon_size" android:layout_height="@dimen/icon_size" + android:layout_marginEnd="12dp" android:contentDescription="@null" android:src="@drawable/check" android:visibility="@{model.selected ? View.VISIBLE : View.GONE, default=gone}" From c50acdf8bc4f176b6cb540094f48465f9d54e2c7 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 17 Feb 2026 17:21:34 +0100 Subject: [PATCH 460/593] Add log if foreground service fails to start + finish call activity in onResume if no more call to display --- .../compatibility/Api28Compatibility.kt | 4 +++- .../compatibility/Api34Compatibility.kt | 4 +++- .../linphone/compatibility/Compatibility.kt | 6 +++--- .../notifications/NotificationsManager.kt | 20 +++++++++++++++---- .../java/org/linphone/ui/call/CallActivity.kt | 5 +++++ 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt index e58cc06258..91e0b5cf70 100644 --- a/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt @@ -34,15 +34,17 @@ class Api28Compatibility { companion object { private const val TAG = "[API 28 Compatibility]" - fun startServiceForeground(service: Service, id: Int, notification: Notification) { + fun startServiceForeground(service: Service, id: Int, notification: Notification): Boolean { try { service.startForeground( id, notification ) + return true } catch (e: Exception) { Log.e("$TAG Can't start service as foreground! $e") } + return false } fun enterPipMode(activity: Activity): Boolean { diff --git a/app/src/main/java/org/linphone/compatibility/Api34Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api34Compatibility.kt index 17eed9de73..285340afa7 100644 --- a/app/src/main/java/org/linphone/compatibility/Api34Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api34Compatibility.kt @@ -44,16 +44,18 @@ class Api34Compatibility { id: Int, notification: Notification, foregroundServiceType: Int - ) { + ): Boolean { try { service.startForeground( id, notification, foregroundServiceType ) + return true } catch (e: Exception) { Log.e("$TAG Can't start service as foreground! $e") } + return false } fun hasFullScreenIntentPermission(context: Context): Boolean { diff --git a/app/src/main/java/org/linphone/compatibility/Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Compatibility.kt index ba3b4c732c..3ed91c9a4f 100644 --- a/app/src/main/java/org/linphone/compatibility/Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Compatibility.kt @@ -52,16 +52,16 @@ class Compatibility { id: Int, notification: Notification, foregroundServiceType: Int - ) { + ): Boolean { if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) { - Api34Compatibility.startServiceForeground( + return Api34Compatibility.startServiceForeground( service, id, notification, foregroundServiceType ) } else { - Api28Compatibility.startServiceForeground(service, id, notification) + return Api28Compatibility.startServiceForeground(service, id, notification) } } diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 779bf9e4f7..2b5e5ea81d 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -805,12 +805,15 @@ class NotificationsManager Log.i( "$TAG Service found, starting it as foreground using notification ID [$INCOMING_CALL_ID] with type PHONE_CALL" ) - Compatibility.startServiceForeground( + val success = Compatibility.startServiceForeground( service, INCOMING_CALL_ID, notification, Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL ) + if (!success) { + Log.e("$TAG Failed to start incoming call foreground service!") + } notificationsMap[INCOMING_CALL_ID] = notification currentInCallServiceNotificationId = INCOMING_CALL_ID inCallServiceForegroundNotificationPublished = true @@ -926,12 +929,15 @@ class NotificationsManager Log.i( "$TAG Service found, starting it as foreground using notification ID [${notifiable.notificationId}] with type(s) [${foregroundServiceTypeMaskToString(mask)}]($mask)" ) - Compatibility.startServiceForeground( + val success = Compatibility.startServiceForeground( service, notifiable.notificationId, notification, mask ) + if (!success) { + Log.e("$TAG Failed to start call foreground service!") + } notificationsMap[notifiable.notificationId] = notification currentInCallServiceNotificationId = notifiable.notificationId inCallServiceForegroundNotificationPublished = true @@ -977,12 +983,15 @@ class NotificationsManager Log.i( "$TAG Service found, starting it as foreground using dummy notification ID [$DUMMY_NOTIF_ID]" ) - Compatibility.startServiceForeground( + val success = Compatibility.startServiceForeground( service, DUMMY_NOTIF_ID, notification, Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL ) + if (!success) { + Log.e("$TAG Failed to start dummy call foreground service!") + } notificationsMap[INCOMING_CALL_ID] = notification currentInCallServiceNotificationId = DUMMY_NOTIF_ID inCallServiceForegroundNotificationPublished = true @@ -1810,12 +1819,15 @@ class NotificationsManager Log.i( "$TAG Keep alive for third party accounts Service found, starting it as foreground using notification ID [$KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID] with type [SPECIAL_USE]" ) - Compatibility.startServiceForeground( + val success = Compatibility.startServiceForeground( service, KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID, notification, Compatibility.FOREGROUND_SERVICE_TYPE_SPECIAL_USE ) + if (!success) { + Log.e("$TAG Failed to start keep alive foreground service!") + } currentKeepAliveThirdPartyAccountsForegroundServiceNotificationId = KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID } else { Log.w("$TAG Keep alive for third party accounts Service hasn't started yet...") diff --git a/app/src/main/java/org/linphone/ui/call/CallActivity.kt b/app/src/main/java/org/linphone/ui/call/CallActivity.kt index 56ff88922c..f9491bfcfe 100644 --- a/app/src/main/java/org/linphone/ui/call/CallActivity.kt +++ b/app/src/main/java/org/linphone/ui/call/CallActivity.kt @@ -376,6 +376,11 @@ class CallActivity : GenericActivity() { if (::callViewModel.isInitialized) { callViewModel.pipMode.value = isInPipMode } + + if (callsViewModel.callsCount.value == 0) { + Log.w("$TAG Call activity is being resumed but no call was found, finishing activity") + finish() + } } override fun onPause() { From 83c8cfd4944d850789232ac1e896c8d6c7016ab3 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 18 Feb 2026 14:38:28 +0100 Subject: [PATCH 461/593] Removed old transifex configuration folder & file --- .tx/config | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .tx/config diff --git a/.tx/config b/.tx/config deleted file mode 100644 index 93225826fd..0000000000 --- a/.tx/config +++ /dev/null @@ -1,11 +0,0 @@ -[main] -host = https://www.transifex.com -lang_map = fr_CA:fr-rCA,pt_BR:pt-rBR,zh_CN:zh-rCN,zh_HK:zh-rHK,zh_TW:zh-rTW,da_DK:da-rDK,sv_SE:sv-rSE,kn_IN:kn-rIN,nl_NL:nl-rNL,en_NL:en-rNL,he:iw -minimum_perc = 1 -type = ANDROID - -[linphone-android.stringsxml] -file_filter = app/src/main/res/values-/strings.xml -source_file = app/src/main/res/values/strings.xml -source_lang = en - From 521cf5e3c165aa74f019cfd9a2c533ec645402cf Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 18 Feb 2026 14:52:32 +0100 Subject: [PATCH 462/593] Added missing setShowWhen() to notification + fixed crash when going into call recordings --- .../java/org/linphone/notifications/NotificationsManager.kt | 4 +++- app/src/main/res/values-land/dimen.xml | 1 - app/src/main/res/values/dimen.xml | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 2b5e5ea81d..befd5a4745 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -108,7 +108,7 @@ class NotificationsManager private const val ACCOUNT_ERROR_TAG = "Account Error" private const val MISSED_CALL_TAG = "Missed call" - const val CHAT_NOTIFICATIONS_GROUP = "CHAT_NOTIF_GROUP" + private const val CHAT_NOTIFICATIONS_GROUP = "CHAT_NOTIF_GROUP" private const val INCOMING_CALL_ID = 1 private const val DUMMY_NOTIF_ID = 3 @@ -1329,6 +1329,7 @@ class NotificationsManager return notifiableMessage } + @SuppressLint("FullScreenIntentPolicy") @WorkerThread private fun createCallNotification( call: Call, @@ -1425,6 +1426,7 @@ class NotificationsManager setPriority(NotificationCompat.PRIORITY_HIGH) } setWhen(call.callLog.startDate * 1000) // Linphone timestamps are in seconds + setShowWhen(true) setAutoCancel(false) setOngoing(true) setContentIntent(pendingIntent) diff --git a/app/src/main/res/values-land/dimen.xml b/app/src/main/res/values-land/dimen.xml index 635c1ecb13..985b2b99e0 100644 --- a/app/src/main/res/values-land/dimen.xml +++ b/app/src/main/res/values-land/dimen.xml @@ -8,7 +8,6 @@ 600dp 600dp 0dp - 20dp 175dp \ No newline at end of file diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index ddc4997e77..001d3e61bc 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -46,6 +46,7 @@ 20dp 20dp + 20dp 54dp 55dp 70dp From 1f36852f372e2b246643acd7b0a3cd333a318778 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 18 Feb 2026 15:05:10 +0100 Subject: [PATCH 463/593] Use incoming call notification ID instead of hard-coded value --- .../notifications/NotificationsManager.kt | 70 ++++++++----------- 1 file changed, 30 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index befd5a4745..62e01e5b42 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -110,7 +110,6 @@ class NotificationsManager private const val MISSED_CALL_TAG = "Missed call" private const val CHAT_NOTIFICATIONS_GROUP = "CHAT_NOTIF_GROUP" - private const val INCOMING_CALL_ID = 1 private const val DUMMY_NOTIF_ID = 3 private const val KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID = 5 private const val ACCOUNT_REGISTRATION_ERROR_ID = 7 @@ -224,14 +223,8 @@ class NotificationsManager showCallNotification(call, false) } Call.State.Connected -> { - if (call.dir == Call.Dir.Incoming) { - Log.i( - "$TAG Connected call was incoming (so it was answered), removing incoming call notification" - ) - removeIncomingCallNotification() - } Log.i( - "$TAG Showing connected call notification for [${call.remoteAddress.asStringUriOnly()}]" + "$TAG Updating incoming call notification to active call for [${call.remoteAddress.asStringUriOnly()}]" ) showCallNotification(call, false) } @@ -257,7 +250,7 @@ class NotificationsManager Log.w("$TAG We are waiting for service to be started as foreground, starting it now") showCallNotification(call, false) } - removeIncomingCallNotification() + removeIncomingCallNotificationIfAny(call) } else { Log.i( "$TAG Removing terminated/declined call notification for [${remoteSipAddress.asStringUriOnly()}]" @@ -681,8 +674,11 @@ class NotificationsManager } @WorkerThread - fun removeIncomingCallNotification() { - if (currentInCallServiceNotificationId == INCOMING_CALL_ID) { + fun removeIncomingCallNotificationIfAny(call: Call) { + val notifiable = getNotifiableForCall(call) + val notificationId = notifiable.notificationId + + if (currentInCallServiceNotificationId == notificationId) { if (inCallService != null) { Log.i( "$TAG Service found, stopping it as foreground before cancelling notification" @@ -698,7 +694,7 @@ class NotificationsManager ) } - cancelNotification(INCOMING_CALL_ID) + cancelNotification(notificationId) currentlyRingingCallRemoteAddress = null } @@ -735,9 +731,9 @@ class NotificationsManager currentlyRingingCallRemoteAddress = call.remoteAddress if (currentInCallServiceNotificationId == -1) { Log.i("$TAG No current in-call foreground service notification found, using this one") - showIncomingCallForegroundServiceNotification(notification) + showIncomingCallForegroundServiceNotification(notifiable.notificationId, notification) } else { - notify(INCOMING_CALL_ID, notification) + notify(notifiable.notificationId, notification) } } else { if (currentInCallServiceNotificationId == -1) { @@ -797,27 +793,27 @@ class NotificationsManager } @WorkerThread - private fun showIncomingCallForegroundServiceNotification(notification: Notification) { + private fun showIncomingCallForegroundServiceNotification(notificationId: Int, notification: Notification) { Log.i("$TAG Trying to start foreground Service using incoming call notification") val service = inCallService if (service != null) { if (Compatibility.isPostNotificationsPermissionGranted(context)) { Log.i( - "$TAG Service found, starting it as foreground using notification ID [$INCOMING_CALL_ID] with type PHONE_CALL" + "$TAG Service found, starting it as foreground using notification ID [$notificationId] with type PHONE_CALL" ) val success = Compatibility.startServiceForeground( service, - INCOMING_CALL_ID, + notificationId, notification, Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL ) if (!success) { Log.e("$TAG Failed to start incoming call foreground service!") } - notificationsMap[INCOMING_CALL_ID] = notification - currentInCallServiceNotificationId = INCOMING_CALL_ID + notificationsMap[notificationId] = notification + currentInCallServiceNotificationId = notificationId inCallServiceForegroundNotificationPublished = true - Log.i("$TAG Incoming call notification with ID [$INCOMING_CALL_ID] has been used to start service as foreground") + Log.i("$TAG Incoming call notification with ID [$notificationId] has been used to start service as foreground") if (waitForInCallServiceForegroundToStopIt) { Log.i("$TAG We were waiting for foreground service to be started to stop it, doing it") @@ -833,14 +829,15 @@ class NotificationsManager @WorkerThread private fun startInCallForegroundService(call: Call) { + val notifiable = getNotifiableForCall(call) + val notificationId = notifiable.notificationId + if (LinphoneUtils.isCallIncoming(call.state)) { - val notification = notificationsMap[INCOMING_CALL_ID] + val notification = notificationsMap[notificationId] if (notification != null) { - showIncomingCallForegroundServiceNotification(notification) + showIncomingCallForegroundServiceNotification(notificationId, notification) } else { - Log.w( - "$TAG Failed to find notification for incoming call with ID [$INCOMING_CALL_ID]" - ) + Log.w("$TAG Failed to find notification for incoming call with ID [$notificationId]") } return } @@ -861,12 +858,8 @@ class NotificationsManager return } - val notifiable = getNotifiableForCall(call) - val notificationId = notifiable.notificationId val notification = if (notificationsMap.containsKey(notificationId)) { notificationsMap[notificationId] - } else if (notificationsMap.containsKey(INCOMING_CALL_ID)) { - notificationsMap[INCOMING_CALL_ID] } else { Log.w("$TAG Failed to find a notification for call [${call.remoteAddress.asStringUriOnly()}] in map") null @@ -992,7 +985,7 @@ class NotificationsManager if (!success) { Log.e("$TAG Failed to start dummy call foreground service!") } - notificationsMap[INCOMING_CALL_ID] = notification + notificationsMap[DUMMY_NOTIF_ID] = notification currentInCallServiceNotificationId = DUMMY_NOTIF_ID inCallServiceForegroundNotificationPublished = true Log.i("$TAG Dummy notification with ID [$DUMMY_NOTIF_ID] has been used to start service as foreground") @@ -1462,14 +1455,11 @@ class NotificationsManager ) { val isIncoming = LinphoneUtils.isCallIncoming(call.state) - val notification = if (isIncoming) { - notificationsMap[INCOMING_CALL_ID] - } else { - notificationsMap[notifiable.notificationId] - } + val notificationId = notifiable.notificationId + val notification = notificationsMap[notificationId] if (notification == null) { Log.w( - "$TAG Failed to find notification with ID [${notifiable.notificationId}], creating a new one" + "$TAG Failed to find notification with ID [$notificationId], creating a new one" ) showCallNotification(call, isIncoming, friend) return @@ -1485,16 +1475,16 @@ class NotificationsManager ) if (isIncoming) { if (!currentlyDisplayedIncomingCallFragment) { - Log.i("$TAG Updating incoming call notification with ID [$INCOMING_CALL_ID]") - notify(INCOMING_CALL_ID, newNotification) + Log.i("$TAG Updating incoming call notification with ID [$notificationId]") + notify(notificationId, newNotification) } else { Log.i( "$TAG Incoming call fragment is visible, do not re-send an incoming call notification" ) } } else { - Log.i("$TAG Updating call notification with ID [${notifiable.notificationId}]") - notify(notifiable.notificationId, newNotification) + Log.i("$TAG Updating call notification with ID [$notificationId]") + notify(notificationId, newNotification) } } From 37200ecf8fa3420a4ddb62555ec3ffbdb86e5952 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 23 Feb 2026 09:56:29 +0100 Subject: [PATCH 464/593] Show error notification if in-call foreground service doesn't starts successfully to let user know there is an issue and clicking on the notification will fix it --- CHANGELOG.md | 1 + .../notifications/NotificationsManager.kt | 184 +++++++++++++----- .../java/org/linphone/ui/call/CallActivity.kt | 4 + .../java/org/linphone/utils/LinphoneUtils.kt | 8 + app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 6 files changed, 154 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ec704c7dc..9493f4b91e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Group changes to describe their impact on the project, as follows: - Improved UI on tablets with screen sw600dp and higher, will look more like our desktop app - Improved navigation within app when using a keyboard - Now loading media/documents contents in conversation by chunks (instead of all of them at once) +- If in-call foreground service doesn't start, show an error notification and clicking on it will fix the issue (by bringing Linphone in foreground and re-starting the foreground service) - Simplified audio device name in settings - Reworked some settings (moved calls related ones from advanced settings to advanced calls settings) - Removed menu to access account profile, button is now directly available from drawer menu diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 62e01e5b42..6bde597276 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -106,6 +106,7 @@ class NotificationsManager const val CHAT_TAG = "Chat" private const val ACCOUNT_ERROR_TAG = "Account Error" + private const val IN_CALL_ERROR_TAG = "Call Error" private const val MISSED_CALL_TAG = "Missed call" private const val CHAT_NOTIFICATIONS_GROUP = "CHAT_NOTIF_GROUP" @@ -113,6 +114,7 @@ class NotificationsManager private const val DUMMY_NOTIF_ID = 3 private const val KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID = 5 private const val ACCOUNT_REGISTRATION_ERROR_ID = 7 + private const val IN_CALL_FOREGROUND_SERVICE_ERROR_ID = 8 private const val MISSED_CALL_ID = 10 } @@ -226,13 +228,14 @@ class NotificationsManager Log.i( "$TAG Updating incoming call notification to active call for [${call.remoteAddress.asStringUriOnly()}]" ) + currentlyRingingCallRemoteAddress = null showCallNotification(call, false) } Call.State.StreamsRunning -> { val notifiable = getNotifiableForCall(call) if (notifiable.notificationId == currentInCallServiceNotificationId) { Log.i( - "$TAG Update foreground service type in case video was enabled/disabled since last time" + "$TAG Update foreground Service type in case video was enabled/disabled since last time" ) startInCallForegroundService(call) } @@ -282,12 +285,17 @@ class NotificationsManager override fun onLastCallEnded(core: Core) { Log.i("$TAG Last call ended") if (inCallServiceForegroundNotificationPublished) { - Log.i("$TAG Stopping foreground service") + Log.i("$TAG Stopping foreground Service") stopInCallForegroundService() } else { Log.i("$TAG In-Call service was never started as foreground, waiting for it to be started to stop it") waitForInCallServiceForegroundToStopIt = true } + + if (notificationsMap.containsKey(IN_CALL_FOREGROUND_SERVICE_ERROR_ID)) { + Log.i("$TAG Removing in-call foreground Service error notification") + cancelNotification(IN_CALL_FOREGROUND_SERVICE_ERROR_ID, IN_CALL_ERROR_TAG) + } } @WorkerThread @@ -673,6 +681,26 @@ class NotificationsManager coreContext.contactsManager.removeListener(contactsListener) } + @WorkerThread + fun showInCallForegroundServiceNotificationIfNeeded() { + if (currentInCallServiceNotificationId == -1) { + Log.w("$TAG No current in-call foreground Service notification found, try to create it now") + val call = coreContext.core.currentCall ?: coreContext.core.calls.find { + LinphoneUtils.isCallActive(it.state) + } ?: coreContext.core.calls.find { + LinphoneUtils.isCallPaused(it.state) + } + if (call != null) { + Log.i("$TAG Using call [${call.remoteAddress.asStringUriOnly()}] for foreground Service notification") + showCallNotification(call, LinphoneUtils.isCallIncoming(call.state)) + } else { + Log.w("$TAG No active call found for foreground Service notification, aborting") + } + } else { + Log.i("$TAG There is already a foreground Service notification for a call, nothing to do") + } + } + @WorkerThread fun removeIncomingCallNotificationIfAny(call: Call) { val notifiable = getNotifiableForCall(call) @@ -730,14 +758,14 @@ class NotificationsManager if (isIncoming) { currentlyRingingCallRemoteAddress = call.remoteAddress if (currentInCallServiceNotificationId == -1) { - Log.i("$TAG No current in-call foreground service notification found, using this one") + Log.i("$TAG No current in-call foreground Service notification found, using this one") showIncomingCallForegroundServiceNotification(notifiable.notificationId, notification) } else { notify(notifiable.notificationId, notification) } } else { if (currentInCallServiceNotificationId == -1) { - Log.i("$TAG No current in-call foreground service notification found, using this one") + Log.i("$TAG No current in-call foreground Service notification found, using this one") showInCallForegroundServiceNotification(call, notifiable, notification) } else { notify(notifiable.notificationId, notification) @@ -807,20 +835,21 @@ class NotificationsManager notification, Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL ) - if (!success) { - Log.e("$TAG Failed to start incoming call foreground service!") - } - notificationsMap[notificationId] = notification - currentInCallServiceNotificationId = notificationId - inCallServiceForegroundNotificationPublished = true - Log.i("$TAG Incoming call notification with ID [$notificationId] has been used to start service as foreground") - - if (waitForInCallServiceForegroundToStopIt) { - Log.i("$TAG We were waiting for foreground service to be started to stop it, doing it") - stopInCallForegroundService() + if (success) { + notificationsMap[notificationId] = notification + currentInCallServiceNotificationId = notificationId + inCallServiceForegroundNotificationPublished = true + Log.i("$TAG Incoming call notification with ID [$notificationId] has been used to start service as foreground") + + if (waitForInCallServiceForegroundToStopIt) { + Log.i("$TAG We were waiting for foreground Service to be started to stop it, doing it") + stopInCallForegroundService() + } + } else { + Log.e("$TAG Failed to start incoming call foreground Service!") } } else { - Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground service!") + Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground Service!") } } else { Log.w("$TAG Core Foreground Service hasn't started yet...") @@ -853,7 +882,7 @@ class NotificationsManager val channel = notificationManager.getNotificationChannel(channelId) val importance = channel?.importance ?: NotificationManagerCompat.IMPORTANCE_NONE if (importance == NotificationManagerCompat.IMPORTANCE_NONE) { - Log.e("$TAG Calls channel has been disabled, can't start foreground service!") + Log.e("$TAG Calls channel has been disabled, can't start foreground Service!") stopInCallForegroundService() return } @@ -897,7 +926,7 @@ class NotificationsManager ) { mask = mask or Compatibility.FOREGROUND_SERVICE_TYPE_MICROPHONE Log.i( - "$TAG RECORD_AUDIO permission has been granted, adding FOREGROUND_SERVICE_TYPE_MICROPHONE to foreground Service types mask" + "$TAG RECORD_AUDIO permission has been granted, adding MICROPHONE to foreground Service types mask" ) } val isSendingVideo = when (call.currentParams.videoDirection) { @@ -912,7 +941,7 @@ class NotificationsManager ) { mask = mask or Compatibility.FOREGROUND_SERVICE_TYPE_CAMERA Log.i( - "$TAG CAMERA permission has been granted, adding FOREGROUND_SERVICE_TYPE_CAMERA to foreground Service types mask" + "$TAG CAMERA permission has been granted, adding CAMERA to foreground Service types mask" ) } } @@ -928,20 +957,35 @@ class NotificationsManager notification, mask ) - if (!success) { - Log.e("$TAG Failed to start call foreground service!") - } - notificationsMap[notifiable.notificationId] = notification - currentInCallServiceNotificationId = notifiable.notificationId - inCallServiceForegroundNotificationPublished = true - Log.i("$TAG Call notification with ID [${notifiable.notificationId}] has been used to start service as foreground") + if (success) { + if (notificationsMap.containsKey(IN_CALL_FOREGROUND_SERVICE_ERROR_ID)) { + Log.i("$TAG Removing previous in-call foreground Service error notification") + cancelNotification(IN_CALL_FOREGROUND_SERVICE_ERROR_ID, IN_CALL_ERROR_TAG) + } - if (waitForInCallServiceForegroundToStopIt) { - Log.i("$TAG We were waiting for foreground service to be started to stop it, doing it") - stopInCallForegroundService() + notificationsMap[notifiable.notificationId] = notification + currentInCallServiceNotificationId = notifiable.notificationId + inCallServiceForegroundNotificationPublished = true + Log.i("$TAG Call notification with ID [${notifiable.notificationId}] has been used to start service as foreground") + + if (waitForInCallServiceForegroundToStopIt) { + Log.i("$TAG We were waiting for foreground Service to be started to stop it, doing it") + stopInCallForegroundService() + } + } else { + Log.e("$TAG Failed to start call foreground Service!") + // In case of incoming call the notification ID would be in the map + // so we have to remove it as notification is no longer displayed + if (notificationsMap.containsKey(notifiable.notificationId)) { + notificationsMap.remove(notifiable.notificationId) + } + if (currentInCallServiceNotificationId == notifiable.notificationId) { + currentInCallServiceNotificationId = -1 + } + showInCallForegroundServiceErrorNotification() } } else { - Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground service!") + Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground Service!") } } @@ -982,15 +1026,16 @@ class NotificationsManager notification, Compatibility.FOREGROUND_SERVICE_TYPE_PHONE_CALL ) - if (!success) { - Log.e("$TAG Failed to start dummy call foreground service!") + if (success) { + notificationsMap[DUMMY_NOTIF_ID] = notification + currentInCallServiceNotificationId = DUMMY_NOTIF_ID + inCallServiceForegroundNotificationPublished = true + Log.i("$TAG Dummy notification with ID [$DUMMY_NOTIF_ID] has been used to start service as foreground") + } else { + Log.e("$TAG Failed to start dummy call foreground Service!") } - notificationsMap[DUMMY_NOTIF_ID] = notification - currentInCallServiceNotificationId = DUMMY_NOTIF_ID - inCallServiceForegroundNotificationPublished = true - Log.i("$TAG Dummy notification with ID [$DUMMY_NOTIF_ID] has been used to start service as foreground") } else { - Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground service!") + Log.e("$TAG POST_NOTIFICATIONS permission isn't granted, don't start foreground Service!") } } else { Log.w("$TAG Core Foreground Service hasn't started yet...") @@ -1001,13 +1046,15 @@ class NotificationsManager private fun stopInCallForegroundService() { val service = inCallService if (service != null) { - Log.i( - "$TAG Stopping foreground Service (was using notification ID [$currentInCallServiceNotificationId])" - ) - service.stopForeground(STOP_FOREGROUND_REMOVE) - service.stopSelf() - inCallServiceForegroundNotificationPublished = false - waitForInCallServiceForegroundToStopIt = false + if (currentInCallServiceNotificationId != -1) { + Log.i( + "$TAG Stopping foreground Service (was using notification ID [$currentInCallServiceNotificationId])" + ) + service.stopForeground(STOP_FOREGROUND_REMOVE) + service.stopSelf() + inCallServiceForegroundNotificationPublished = false + waitForInCallServiceForegroundToStopIt = false + } } else { Log.w("$TAG Can't stop foreground Service & notif, no Service was found") } @@ -1228,6 +1275,44 @@ class NotificationsManager } } + @WorkerThread + private fun showInCallForegroundServiceErrorNotification() { + if (Compatibility.isPostNotificationsPermissionGranted(context)) { + val pendingIntent = TaskStackBuilder.create(context).run { + addNextIntentWithParentStack( + Intent(context, CallActivity::class.java).apply { + action = Intent.ACTION_MAIN // Needed as well + flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT + } + ) + getPendingIntent( + IN_CALL_FOREGROUND_SERVICE_ERROR_ID, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + )!! + } + + val notification = NotificationCompat.Builder( + context, + context.getString(R.string.notification_channel_account_error_id) + ) + .setContentTitle(context.getString(R.string.notification_in_call_foreground_service_error_title)) + .setContentText(context.getString(R.string.notification_in_call_foreground_service_error_message)) + .setSmallIcon(R.drawable.warning_circle) + .setAutoCancel(true) + .setOngoing(false) + .setCategory(NotificationCompat.CATEGORY_ERROR) + .setWhen(System.currentTimeMillis()) + .setShowWhen(true) + .setContentIntent(pendingIntent) + .build() + + val notificationId = IN_CALL_FOREGROUND_SERVICE_ERROR_ID + Log.i("$TAG Showing in-call foreground Service error notification with ID [$notificationId]") + notificationsMap[notificationId] = notification + notify(notificationId, notification, IN_CALL_ERROR_TAG) + } + } + @SuppressLint("MissingPermission") @WorkerThread private fun notify(id: Int, notification: Notification, tag: String? = null) { @@ -1573,7 +1658,12 @@ class NotificationsManager val address = call.remoteAddress.asStringUriOnly() val notifiable: Notifiable? = callNotificationsMap[address] if (notifiable != null) { - cancelNotification(notifiable.notificationId) + if (notificationsMap.containsKey(notifiable.notificationId)) { + cancelNotification(notifiable.notificationId) + } else if (notificationsMap.containsKey(IN_CALL_FOREGROUND_SERVICE_ERROR_ID)) { + Log.i("$TAG Removing previous in-call foreground Service error notification") + cancelNotification(IN_CALL_FOREGROUND_SERVICE_ERROR_ID, IN_CALL_ERROR_TAG) + } callNotificationsMap.remove(address) } else { Log.w("$TAG No notification found for call with remote address [$address]") @@ -1777,7 +1867,7 @@ class NotificationsManager val importance = channel?.importance ?: NotificationManagerCompat.IMPORTANCE_NONE if (importance == NotificationManagerCompat.IMPORTANCE_NONE) { Log.e( - "$TAG Keep alive for third party accounts Service channel has been disabled, can't start foreground service!" + "$TAG Keep alive for third party accounts Service channel has been disabled, can't start foreground Service!" ) return } @@ -1818,7 +1908,7 @@ class NotificationsManager Compatibility.FOREGROUND_SERVICE_TYPE_SPECIAL_USE ) if (!success) { - Log.e("$TAG Failed to start keep alive foreground service!") + Log.e("$TAG Failed to start keep alive foreground Service!") } currentKeepAliveThirdPartyAccountsForegroundServiceNotificationId = KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID } else { diff --git a/app/src/main/java/org/linphone/ui/call/CallActivity.kt b/app/src/main/java/org/linphone/ui/call/CallActivity.kt index f9491bfcfe..a6aced1b98 100644 --- a/app/src/main/java/org/linphone/ui/call/CallActivity.kt +++ b/app/src/main/java/org/linphone/ui/call/CallActivity.kt @@ -381,6 +381,10 @@ class CallActivity : GenericActivity() { Log.w("$TAG Call activity is being resumed but no call was found, finishing activity") finish() } + + coreContext.postOnCoreThread { + coreContext.notificationsManager.showInCallForegroundServiceNotificationIfNeeded() + } } override fun onPause() { diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 73afcec004..32bcd6c37e 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -266,6 +266,14 @@ class LinphoneUtils { } } + @AnyThread + fun isCallActive(callState: Call.State): Boolean { + return when (callState) { + Call.State.Connected, Call.State.StreamsRunning, Call.State.UpdatedByRemote, Call.State.Updating -> true + else -> false + } + } + @WorkerThread fun getCallErrorInfoToast(call: Call): String { val errorInfo = call.errorInfo diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 9cb66ced28..2122460152 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -76,6 +76,8 @@ Désactiver haut-parleur Compte %s en erreur ! Ouvrez &appName; pour rafraîchir la connexion + Votre correspondant ne vous entend pas ! + Cliquez sur cette notification pour corriger le problème Bienvenue diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4fa51ca51f..79a147bfdd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -118,6 +118,8 @@ Turn off speaker Account %s registration failed! Open &appName; to refresh the registration + Your correspondent does not hear you! + Click on this notification to fix it Welcome From 70183f76fc2253d5d64594962d4f920dbe12ed8e Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 23 Feb 2026 10:38:30 +0100 Subject: [PATCH 465/593] Fixed proximity sensor disabled after app has been put in background and then resumed by clicking on the call notification --- .../main/java/org/linphone/ui/call/CallActivity.kt | 1 + .../ui/call/viewmodel/CurrentCallViewModel.kt | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/call/CallActivity.kt b/app/src/main/java/org/linphone/ui/call/CallActivity.kt index a6aced1b98..b5ab81ac65 100644 --- a/app/src/main/java/org/linphone/ui/call/CallActivity.kt +++ b/app/src/main/java/org/linphone/ui/call/CallActivity.kt @@ -384,6 +384,7 @@ class CallActivity : GenericActivity() { coreContext.postOnCoreThread { coreContext.notificationsManager.showInCallForegroundServiceNotificationIfNeeded() + callViewModel.updateProximitySensor() } } diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index fc410ab76c..9785cc7384 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -1553,27 +1553,39 @@ class CurrentCallViewModel } @WorkerThread - private fun updateProximitySensor() { + fun updateProximitySensor() { if (::currentCall.isInitialized) { val callState = currentCall.state + Log.i("$TAG Call is in state [$callState], enabling/disabling proximity sensor if needed") + if (LinphoneUtils.isCallIncoming(callState)) { + Log.i("$TAG Call is incoming, disabling proximity sensor") proximitySensorEnabled.postValue(false) } else if (LinphoneUtils.isCallOutgoing(callState)) { val videoEnabled = currentCall.params.isVideoEnabled + if (videoEnabled) { + Log.i("$TAG Call is outgoing and video is enabled, disabling proximity sensor") + } else { + Log.i("$TAG Call is outgoing and video is disabled, enabling proximity sensor") + } proximitySensorEnabled.postValue(!videoEnabled) } else { if (isSendingVideo.value == true || isReceivingVideo.value == true) { + Log.i("$TAG Video is being sent and/or received, disabling proximity sensor") proximitySensorEnabled.postValue(false) } else { val outputAudioDevice = currentCall.outputAudioDevice ?: coreContext.core.outputAudioDevice if (outputAudioDevice != null && outputAudioDevice.type == AudioDevice.Type.Earpiece) { + Log.i("$TAG Audio device is earpiece, enabling proximity sensor") proximitySensorEnabled.postValue(true) } else { + Log.i("$TAG Audio device is [${outputAudioDevice?.type}], disabling proximity sensor") proximitySensorEnabled.postValue(false) } } } } else { + Log.w("$TAG No current call, disabling proximity sensor") proximitySensorEnabled.postValue(false) } } From 7685d7e5aae82358d8334607ee755c0dd4ec89b8 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 23 Feb 2026 12:47:17 +0000 Subject: [PATCH 466/593] Updated translations from Weblate --- app/src/main/res/values-de/strings.xml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8a3cae12ec..d42cc14f5e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -551,7 +551,7 @@ Layout Im Gange Klingeln - Eingehend + Eingehender Aktiv Pausieren Fern pausiert @@ -896,4 +896,12 @@ Mikrofon HDMI ICE: %s + Der Klingelton-Auswähler ist nicht verfügbar! + Keine verwaisten Authentifizierungsinformationen gefunden + Verbindungsstatus + Authentifizierungsinformationen löschen, die nicht mehr mit einem Konto verknüpft sind + + %s verwaiste Anmeldeinformation entfernt + %s verwaiste Anmeldeinformationen entfernt + From c492d99b09cb3c7c5c965dac8d1978ba2dbcb8c0 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 23 Feb 2026 13:47:52 +0100 Subject: [PATCH 467/593] Bumped version code for next beta --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0394ddbc12..c0e98a2c77 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -106,7 +106,7 @@ android { applicationId = packageName minSdk = 28 targetSdk = 36 - versionCode = 601005 // 6.01.005 + versionCode = 601006 // 6.01.006 versionName = "6.2.0-beta" manifestPlaceholders["appAuthRedirectScheme"] = packageName From 973398307c1d98f9d3edc2b893fd3d5adf3de21c Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 23 Feb 2026 15:00:27 +0100 Subject: [PATCH 468/593] Fixed wrong constraint in call transfer layout & lists not updated when switching default account --- .../fragment/ConversationsListFragment.kt | 2 +- .../viewmodel/ContactsListViewModel.kt | 9 ++----- .../history/fragment/HistoryListFragment.kt | 2 +- .../meetings/fragment/MeetingsListFragment.kt | 8 +++---- .../res/layout/call_transfer_fragment.xml | 24 +++++++++---------- 5 files changed, 20 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt index 4a7733e4e6..c9279d3a66 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt @@ -100,7 +100,7 @@ class ConversationsListFragment : AbstractMainFragment() { Log.i( "$TAG Default account changed, updating avatar in top bar & re-computing conversations" ) - listViewModel.applyFilter() + listViewModel.filter() } override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? { diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt index 18a9631fe5..f4e3dc8b1e 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactsListViewModel.kt @@ -188,10 +188,7 @@ class ContactsListViewModel override fun filter() { isListFiltered.value = currentFilter.isNotEmpty() coreContext.postOnCoreThread { - applyFilter( - currentFilter, - domainFilter - ) + applyFilter(currentFilter, domainFilter) } } @@ -202,9 +199,7 @@ class ContactsListViewModel areAllContactsDisplayed.postValue(domainFilter.isEmpty()) checkIfDefaultAccountOnDefaultDomain() - coreContext.postOnMainThread { - applyFilter(currentFilter) - } + applyFilter(currentFilter, domainFilter) } } diff --git a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt index 1c498cebca..e0cd5b6a3a 100644 --- a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt @@ -83,7 +83,7 @@ class HistoryListFragment : AbstractMainFragment() { Log.i( "$TAG Default account changed, updating avatar in top bar & re-computing call logs" ) - listViewModel.applyFilter() + listViewModel.filter() } override fun onCreateAnimation(transit: Int, enter: Boolean, nextAnim: Int): Animation? { diff --git a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt index 1f93f4a25f..26fa0f712d 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt @@ -77,7 +77,7 @@ class MeetingsListFragment : AbstractMainFragment() { Log.i( "$TAG Default account changed, updating avatar in top bar & re-computing meetings list" ) - listViewModel.applyFilter() + listViewModel.filter() } } @@ -176,7 +176,7 @@ class MeetingsListFragment : AbstractMainFragment() { meetingViewModelBeingCancelled?.delete() meetingViewModelBeingCancelled = null - listViewModel.applyFilter() + listViewModel.filter() (requireActivity() as GenericActivity).showGreenToast( getString(R.string.meeting_info_deleted_toast), @@ -201,7 +201,7 @@ class MeetingsListFragment : AbstractMainFragment() { } else { Log.i("$TAG Deleting meeting [${model.id}]") model.delete() - listViewModel.applyFilter() + listViewModel.filter() } } ) @@ -213,7 +213,7 @@ class MeetingsListFragment : AbstractMainFragment() { sharedViewModel.forceRefreshMeetingsListEvent.observe(viewLifecycleOwner) { it.consume { Log.i("$TAG We were asked to refresh the meetings list, doing it now") - listViewModel.applyFilter() + listViewModel.filter() } } diff --git a/app/src/main/res/layout/call_transfer_fragment.xml b/app/src/main/res/layout/call_transfer_fragment.xml index b52197c9c1..aa4d1ba1a3 100644 --- a/app/src/main/res/layout/call_transfer_fragment.xml +++ b/app/src/main/res/layout/call_transfer_fragment.xml @@ -229,22 +229,22 @@ app:layout_constraintTop_toBottomOf="@id/results_limit_reached" app:layout_constraintBottom_toBottomOf="parent" /> + + - - Date: Tue, 24 Feb 2026 10:00:52 +0100 Subject: [PATCH 469/593] No longer need to check this at app level, it is now done by the SDK --- .../ui/main/chat/viewmodel/ConversationMediaListViewModel.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt index a7f728062c..dd17e4e7eb 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationMediaListViewModel.kt @@ -105,9 +105,6 @@ class ConversationMediaListViewModel private fun getFileModelsListFromContents(contents: Array): ArrayList { val list = arrayListOf() for (mediaContent in contents) { - // Do not display voice recordings here, even if they are media file - if (mediaContent.isVoiceRecording) continue - val isEncrypted = mediaContent.isFileEncrypted val originalPath = mediaContent.filePath.orEmpty() val path = if (isEncrypted) { From 3ad6c6c8ed0013199015999f98c3215fd8407ae8 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 25 Feb 2026 09:56:59 +0100 Subject: [PATCH 470/593] Access shared media & documents from contact page if a 1-1 chat room is found --- CHANGELOG.md | 1 + .../main/contacts/fragment/ContactFragment.kt | 18 +++++ .../contacts/fragment/ContactsListFragment.kt | 28 +++++++ .../contacts/viewmodel/ContactViewModel.kt | 71 +++++++++++++++- .../main/res/layout/chat_info_fragment.xml | 12 +-- app/src/main/res/layout/contact_fragment.xml | 80 ++++++++++++++++++- .../res/navigation/contacts_nav_graph.xml | 36 +++++++++ 7 files changed, 232 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9493f4b91e..5b7f5973de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Group changes to describe their impact on the project, as follows: - Added keyboard shortcuts on IncomingCallFragment: Ctrl + Shift + A to answer the call, Ctrl + Shift + D to decline it - Added seeking feature to recordings & media player within app - Added PDF preview in conversation (message bubble & documents list) +- Added media/documents access from contact page if a 1-1 conversation with any of the contact SIP addresses is found - Added hover effect when using a mouse (useful for tablets or devices with desktop mode) - Support right click on some items to open bottom sheet/menu - Added toggle speaker action in active call notification diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt index 7a4db5daca..b6f0df787f 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactFragment.kt @@ -115,6 +115,24 @@ class ContactFragment : SlidingPaneChildFragment() { showDeleteConfirmationDialog() } + binding.setGoToSharedMediaClickListener { + if (findNavController().currentDestination?.id == R.id.contactFragment) { + val conversationId = viewModel.existingConversationId.value.orEmpty() + Log.i("$TAG Going to shared media fragment for conversation [$conversationId]") + val action = ContactFragmentDirections.actionContactFragmentToConversationMediaListFragment(conversationId) + findNavController().navigate(action) + } + } + + binding.setGoToSharedDocumentsClickListener { + if (findNavController().currentDestination?.id == R.id.contactFragment) { + val conversationId = viewModel.existingConversationId.value.orEmpty() + Log.i("$TAG Going to shared documents fragment for conversation [$conversationId]") + val action = ContactFragmentDirections.actionContactFragmentToConversationDocumentsListFragment(conversationId) + findNavController().navigate(action) + } + } + sharedViewModel.isSlidingPaneSlideable.observe(viewLifecycleOwner) { slideable -> viewModel.showBackButton.value = slideable } diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt index 8465ba3cb6..83529b6b43 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/ContactsListFragment.kt @@ -50,6 +50,8 @@ import org.linphone.core.FriendList import org.linphone.core.tools.Log import org.linphone.databinding.ContactsListFilterPopupMenuBinding import org.linphone.databinding.ContactsListFragmentBinding +import org.linphone.ui.fileviewer.FileViewerActivity +import org.linphone.ui.fileviewer.MediaViewerActivity import org.linphone.ui.main.MainActivity import org.linphone.ui.main.contacts.adapter.ContactsListAdapter import org.linphone.ui.main.contacts.model.ContactAvatarModel @@ -234,6 +236,32 @@ class ContactsListFragment : AbstractMainFragment() { } } + sharedViewModel.displayFileEvent.observe(viewLifecycleOwner) { + it.consume { bundle -> + if (findNavController().currentDestination?.id == R.id.contactsListFragment) { + val path = bundle.getString("path", "") + val isMedia = bundle.getBoolean("isMedia", false) + if (path.isEmpty()) { + Log.e("$TAG Can't navigate to file viewer for empty path!") + return@consume + } + + Log.i( + "$TAG Navigating to [${if (isMedia) "media" else "file"}] viewer fragment with path [$path]" + ) + if (isMedia) { + val intent = Intent(requireActivity(), MediaViewerActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) + } else { + val intent = Intent(requireActivity(), FileViewerActivity::class.java) + intent.putExtras(bundle) + startActivity(intent) + } + } + } + } + // AbstractMainFragment related listViewModel.title.value = getString(R.string.bottom_navigation_contacts_label) diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt index eb15c30bdb..1957f1a3ba 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactViewModel.kt @@ -99,6 +99,8 @@ class ContactViewModel val videoCallDisabled = MutableLiveData() + val existingConversationId = MutableLiveData() + val operationInProgress = MutableLiveData() val showLongPressMenuForNumberOrAddressEvent: MutableLiveData> by lazy { @@ -207,7 +209,12 @@ class ContactViewModel Log.i("$TAG Conversation [$id] successfully created") chatRoom.removeListener(this) operationInProgress.postValue(false) - goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom))) + + val conversationId = LinphoneUtils.getConversationId(chatRoom) + if (existingConversationId.value.orEmpty().isEmpty()) { + existingConversationId.postValue(conversationId) + } + goToConversationEvent.postValue(Event(conversationId)) } else if (state == ChatRoom.State.CreationFailed) { Log.e("$TAG Conversation [$id] creation has failed!") chatRoom.removeListener(this) @@ -330,6 +337,7 @@ class ContactViewModel sipAddressesAndPhoneNumbers.postValue(addressesAndNumbers) fetchDevicesAndTrust() + lookUpExistingChatRoom() } @UiThread @@ -545,7 +553,12 @@ class ContactViewModel existingChatRoom )}], going to it" ) - goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(existingChatRoom))) + + val conversationId = LinphoneUtils.getConversationId(existingChatRoom) + if (existingConversationId.value.orEmpty().isEmpty()) { + existingConversationId.postValue(conversationId) + } + goToConversationEvent.postValue(Event(conversationId)) } else { Log.i( "$TAG No existing conversation between [$localSipUri] and [$remoteSipUri] was found, let's create it" @@ -558,7 +571,12 @@ class ContactViewModel val id = LinphoneUtils.getConversationId(chatRoom) Log.i("$TAG 1-1 conversation [$id] has been created") operationInProgress.postValue(false) - goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom))) + + val conversationId = LinphoneUtils.getConversationId(chatRoom) + if (existingConversationId.value.orEmpty().isEmpty()) { + existingConversationId.postValue(conversationId) + } + goToConversationEvent.postValue(Event(conversationId)) } else { Log.i("$TAG Conversation isn't in Created state yet, wait for it") chatRoom.addListener(chatRoomListener) @@ -567,7 +585,12 @@ class ContactViewModel val id = LinphoneUtils.getConversationId(chatRoom) Log.i("$TAG Conversation successfully created [$id]") operationInProgress.postValue(false) - goToConversationEvent.postValue(Event(LinphoneUtils.getConversationId(chatRoom))) + + val conversationId = LinphoneUtils.getConversationId(chatRoom) + if (existingConversationId.value.orEmpty().isEmpty()) { + existingConversationId.postValue(conversationId) + } + goToConversationEvent.postValue(Event(conversationId)) } } else { Log.e( @@ -627,4 +650,44 @@ class ContactViewModel devices.postValue(devicesList) } + + @WorkerThread + private fun lookUpExistingChatRoom() { + val account = LinphoneUtils.getDefaultAccount() + if (account != null) { + val params = coreContext.core.createConferenceParams(null) + params.isChatEnabled = true + params.isGroupEnabled = false + params.account = account + + val localAddress = account.params.identityAddress + val addresses = friend.addresses + for (address in addresses) { + val sameDomain = address.domain == corePreferences.defaultDomain && address.domain == account.params.domain + if (account.params.instantMessagingEncryptionMandatory && sameDomain) { + params.securityLevel = Conference.SecurityLevel.EndToEnd + } else if (!account.params.instantMessagingEncryptionMandatory) { + if (LinphoneUtils.isEndToEndEncryptedChatAvailable(coreContext.core)) { + params.securityLevel = Conference.SecurityLevel.EndToEnd + } else { + params.securityLevel = Conference.SecurityLevel.None + } + } + + val participants = arrayOf(address) + val existingChatRoom = coreContext.core.searchChatRoom(params, localAddress, null, participants) + if (existingChatRoom != null) { + val conversationId = LinphoneUtils.getConversationId(existingChatRoom) + Log.i("$TAG Found existing conversation with ID [$conversationId]") + existingConversationId.postValue(conversationId) + return + } + } + + Log.w("$TAG No existing conversation was found for this contact with any of it's SIP addresses") + existingConversationId.postValue("") + } else { + Log.e("$TAG No default account found!") + } + } } diff --git a/app/src/main/res/layout/chat_info_fragment.xml b/app/src/main/res/layout/chat_info_fragment.xml index bd806d1f2d..745d4a6009 100644 --- a/app/src/main/res/layout/chat_info_fragment.xml +++ b/app/src/main/res/layout/chat_info_fragment.xml @@ -416,10 +416,10 @@ style="@style/section_header_style" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="20dp" + android:layout_marginStart="16dp" android:layout_marginTop="16dp" - android:layout_marginEnd="20dp" - android:padding="5dp" + android:layout_marginEnd="16dp" + android:padding="10dp" android:text="@string/conversation_details_media_documents_title" app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintBottom_toTopOf="@id/action_media" @@ -476,10 +476,10 @@ style="@style/section_header_style" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="20dp" + android:layout_marginStart="16dp" android:layout_marginTop="16dp" - android:layout_marginEnd="20dp" - android:padding="5dp" + android:layout_marginEnd="16dp" + android:padding="10dp" android:text="@string/contact_details_actions_title" app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/contact_fragment.xml b/app/src/main/res/layout/contact_fragment.xml index b856082822..b265b50db4 100644 --- a/app/src/main/res/layout/contact_fragment.xml +++ b/app/src/main/res/layout/contact_fragment.xml @@ -15,6 +15,12 @@ + + @@ -55,7 +61,6 @@ app:layout_constraintStart_toEndOf="@id/back" app:layout_constraintTop_toTopOf="parent"/> - + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/action_documents"/> + + + + + + + + + + \ No newline at end of file From 7fe554be3c8c47f445b7e523e049a830b388cd1e Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 2 Mar 2026 10:10:46 +0100 Subject: [PATCH 471/593] Fixed crash due to TextureView API not called from main thread --- .../org/linphone/ui/call/view/RoundCornersTextureView.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/call/view/RoundCornersTextureView.kt b/app/src/main/java/org/linphone/ui/call/view/RoundCornersTextureView.kt index 7dea7b1081..89484339da 100644 --- a/app/src/main/java/org/linphone/ui/call/view/RoundCornersTextureView.kt +++ b/app/src/main/java/org/linphone/ui/call/view/RoundCornersTextureView.kt @@ -25,7 +25,9 @@ import android.graphics.Rect import android.util.AttributeSet import android.view.View import android.view.ViewOutlineProvider +import androidx.annotation.MainThread import androidx.annotation.UiThread +import org.linphone.LinphoneApplication.Companion.coreContext import java.lang.NumberFormatException import org.linphone.R import org.linphone.mediastream.video.capture.CaptureTextureView @@ -86,11 +88,13 @@ class RoundCornersTextureView : CaptureTextureView { } } + @MainThread fun setRadius(radius: Float) { mRadius = radius setRoundCorners() } + @MainThread private fun setRoundCorners() { outlineProvider = object : ViewOutlineProvider() { override fun getOutline(view: View, outline: Outline) { @@ -123,7 +127,9 @@ class RoundCornersTextureView : CaptureTextureView { val previewSize = previewVideoSize if (previewSize.width > 0 && previewSize.height > 0) { - setRoundCorners() + coreContext.postOnMainThread { + setRoundCorners() + } } } } From 1c4f73a9b856cd228bab9d87d807f78e8e485403 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 2 Mar 2026 11:14:34 +0100 Subject: [PATCH 472/593] Show video preview in in-call conversation screen --- CHANGELOG.md | 1 + .../ui/call/fragment/ConversationFragment.kt | 92 +++++++++++++++++++ ..._conference_participants_list_fragment.xml | 1 + .../call_video_local_preview_surface.xml | 20 ++++ .../main/res/layout/calls_list_fragment.xml | 1 + .../res/layout/chat_conversation_fragment.xml | 1 + 6 files changed, 116 insertions(+) create mode 100644 app/src/main/res/layout/call_video_local_preview_surface.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b7f5973de..c1f22f4e7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Group changes to describe their impact on the project, as follows: - one to let edit native contacts Linphone copy in-app instead of opening native addressbook third party app - Added a vu meter for recording & playback volumes (must be enabled in developer settings) - Added support for HDMI audio devices +- Added video preview during in-call conversation ### Changed - No longer follow TelecomManager audio endpoint during calls, using our own routing policy diff --git a/app/src/main/java/org/linphone/ui/call/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/ConversationFragment.kt index c18d716bad..c5c5660cd0 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/ConversationFragment.kt @@ -19,12 +19,20 @@ */ package org.linphone.ui.call.fragment +import android.annotation.SuppressLint import android.content.Intent import android.os.Bundle +import android.view.MotionEvent import android.view.View +import android.view.ViewGroup +import androidx.core.view.doOnLayout +import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController +import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R import org.linphone.core.tools.Log +import org.linphone.ui.call.view.RoundCornersTextureView +import org.linphone.ui.call.viewmodel.CurrentCallViewModel import org.linphone.ui.fileviewer.FileViewerActivity import org.linphone.ui.fileviewer.MediaViewerActivity import org.linphone.ui.main.chat.fragment.ConversationFragment @@ -34,9 +42,48 @@ class ConversationFragment : ConversationFragment() { private const val TAG = "[In-call Conversation Fragment]" } + private lateinit var callViewModel: CurrentCallViewModel + + private lateinit var localPreviewVideoSurface: RoundCornersTextureView + + private var videoPreviewX: Float = 0f + private var videoPreviewY: Float = 0f + + // For moving video preview purposes + private val videoPreviewTouchListener = View.OnTouchListener { view, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + videoPreviewX = view.x - event.rawX + videoPreviewY = view.y - event.rawY + true + } + MotionEvent.ACTION_UP -> { + videoPreviewX = view.x + videoPreviewY = view.y + true + } + MotionEvent.ACTION_MOVE -> { + view.animate() + .x(event.rawX + videoPreviewX) + .y(event.rawY + videoPreviewY) + .setDuration(0) + .start() + true + } + else -> { + view.performClick() + false + } + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + callViewModel = requireActivity().run { + ViewModelProvider(this)[CurrentCallViewModel::class.java] + } + Log.i("$TAG Creating an in-call ConversationFragment") sendMessageViewModel.isCallConversation.value = true viewModel.isCallConversation.value = true @@ -70,5 +117,50 @@ class ConversationFragment : ConversationFragment() { } } } + + val layout = layoutInflater.inflate(R.layout.call_video_local_preview_surface, binding.constraintLayout, false) + binding.constraintLayout.addView(layout) + localPreviewVideoSurface = layout.findViewById(R.id.local_preview_video_surface) + + callViewModel.isSendingVideo.observe(viewLifecycleOwner) { sending -> + coreContext.postOnCoreThread { core -> + core.nativePreviewWindowId = if (sending) { + Log.i("$TAG We are sending video, setting capture preview surface") + localPreviewVideoSurface + } else { + Log.i("$TAG We are not sending video, clearing capture preview surface") + null + } + } + } + } + + override fun onResume() { + super.onResume() + + (binding.root as? ViewGroup)?.doOnLayout { + setupVideoPreview(localPreviewVideoSurface) + } + } + + override fun onPause() { + super.onPause() + + cleanVideoPreview(localPreviewVideoSurface) + } + + @SuppressLint("ClickableViewAccessibility") + private fun setupVideoPreview(localPreviewVideoSurface: RoundCornersTextureView) { + if (requireActivity().isInPictureInPictureMode) { + Log.i("$TAG Activity is in PiP mode, do not move video preview") + return + } + + localPreviewVideoSurface.setOnTouchListener(videoPreviewTouchListener) + } + + @SuppressLint("ClickableViewAccessibility") + private fun cleanVideoPreview(localPreviewVideoSurface: RoundCornersTextureView) { + localPreviewVideoSurface.setOnTouchListener(null) } } diff --git a/app/src/main/res/layout/call_conference_participants_list_fragment.xml b/app/src/main/res/layout/call_conference_participants_list_fragment.xml index eaffd8708b..b790c8a3b2 100644 --- a/app/src/main/res/layout/call_conference_participants_list_fragment.xml +++ b/app/src/main/res/layout/call_conference_participants_list_fragment.xml @@ -100,6 +100,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="10dp" + android:layout_marginBottom="10dp" app:alignTopRight="true" app:displayMode="black_bars" roundCornersRadius="@dimen/call_round_corners_texture_view_radius" diff --git a/app/src/main/res/layout/call_video_local_preview_surface.xml b/app/src/main/res/layout/call_video_local_preview_surface.xml new file mode 100644 index 0000000000..99061f5c87 --- /dev/null +++ b/app/src/main/res/layout/call_video_local_preview_surface.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/calls_list_fragment.xml b/app/src/main/res/layout/calls_list_fragment.xml index c42e46ed7a..3cb191a64b 100644 --- a/app/src/main/res/layout/calls_list_fragment.xml +++ b/app/src/main/res/layout/calls_list_fragment.xml @@ -98,6 +98,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="10dp" + android:layout_marginBottom="10dp" app:alignTopRight="true" app:displayMode="black_bars" roundCornersRadius="@dimen/call_round_corners_texture_view_radius" diff --git a/app/src/main/res/layout/chat_conversation_fragment.xml b/app/src/main/res/layout/chat_conversation_fragment.xml index 22274b5f48..5d9ee2ef9b 100644 --- a/app/src/main/res/layout/chat_conversation_fragment.xml +++ b/app/src/main/res/layout/chat_conversation_fragment.xml @@ -53,6 +53,7 @@ android:layout_height="match_parent"> From b055b534680093c445cac45e042333b7271911e6 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 2 Mar 2026 13:30:09 +0100 Subject: [PATCH 473/593] Show specific label for end-to-end encrypted meetings --- .../call/conference/viewmodel/ConferenceViewModel.kt | 10 ++++++++++ app/src/main/res/layout/call_media_encryption_info.xml | 2 +- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt index 5c1272cce8..c5a850a282 100644 --- a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt @@ -66,6 +66,8 @@ class ConferenceViewModel val isCurrentCallInConference = MutableLiveData() + val isEndToEndEncrypted = MutableLiveData() + val conferenceLayout = MutableLiveData() val screenSharingParticipantName = MutableLiveData() @@ -280,6 +282,10 @@ class ConferenceViewModel isPaused.postValue(!isIn) Log.i("$TAG We [${if (isIn) "are" else "aren't"}] in the conference") + val conferenceSecurityLevel = conference.currentParams.securityLevel + Log.i("$TAG Conference call security level is [$conferenceSecurityLevel]") + isEndToEndEncrypted.postValue(conferenceSecurityLevel == Conference.SecurityLevel.EndToEnd) + subject.postValue(conference.subjectUtf8.orEmpty()) computeParticipants(false) if (conference.participantList.size >= 1) { // we do not count @@ -316,6 +322,10 @@ class ConferenceViewModel } isCurrentCallInConference.postValue(true) + val conferenceSecurityLevel = conf.currentParams.securityLevel + Log.i("$TAG Conference call security level is [$conferenceSecurityLevel]") + isEndToEndEncrypted.postValue(conferenceSecurityLevel == Conference.SecurityLevel.EndToEnd) + conference = conf conference.addListener(conferenceListener) conferenceConfigured = true diff --git a/app/src/main/res/layout/call_media_encryption_info.xml b/app/src/main/res/layout/call_media_encryption_info.xml index 3ab420a079..8fc5cf59d1 100644 --- a/app/src/main/res/layout/call_media_encryption_info.xml +++ b/app/src/main/res/layout/call_media_encryption_info.xml @@ -144,7 +144,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="5dp" - android:text="@{viewModel.isZrtp ? (viewModel.conferenceModel.isCurrentCallInConference ? @string/call_zrtp_point_to_point_encrypted : @string/call_zrtp_end_to_end_encrypted) : @string/call_srtp_point_to_point_encrypted, default=@string/call_zrtp_end_to_end_encrypted}" + android:text="@{viewModel.conferenceModel.isCurrentCallInConference ? (viewModel.conferenceModel.isEndToEndEncrypted ? @string/call_conference_end_to_end_encrypted : (viewModel.isZrtp ? @string/call_zrtp_point_to_point_encrypted : @string/call_srtp_point_to_point_encrypted)) : viewModel.isZrtp ? @string/call_zrtp_end_to_end_encrypted : @string/call_srtp_point_to_point_encrypted, default=@string/call_zrtp_end_to_end_encrypted}" android:textSize="12sp" android:textColor="@color/blue_info_500" android:maxLines="1" diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2122460152..455ece6025 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -723,6 +723,7 @@ En attente du chiffrement… Appel chiffré de bout en bout Appel chiffré de point à point + Conférence chiffrée de bout en bout Faire la vérification à nouveau Vérification nécessaire Appel chiffré de point à point diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 79a147bfdd..b6e1a08b5a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -766,6 +766,7 @@ Waiting for encryption… End-to-end encrypted by ZRTP Point-to-point encrypted by ZRTP + End-to-end encrypted Validate ZRTP SAS again Validation required Point-to-point encrypted by SRTP From b5743a73949645d02a80b53ab8692ba9528f3a80 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 2 Mar 2026 15:53:04 +0100 Subject: [PATCH 474/593] Prevent conference related chat rooms to be added to conversations list --- .../ui/main/chat/viewmodel/ConversationsListViewModel.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationsListViewModel.kt index 63879a2e0a..39e7456233 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationsListViewModel.kt @@ -253,6 +253,14 @@ class ConversationsListViewModel return } + val conferenceInfo = chatRoom.conferenceInfo + if (conferenceInfo != null) { + Log.w( + "$TAG Chat room with identifier [$identifier] was created but not displaying it because it is related to a conference" + ) + return + } + val hideEmptyChatRooms = coreContext.core.config.getBool("misc", "hide_empty_chat_rooms", true) // Hide empty chat rooms only applies to 1-1 conversations if (hideEmptyChatRooms && !LinphoneUtils.isChatRoomAGroup(chatRoom) && chatRoom.lastMessageInHistory == null) { From d37f33a217185432620efe631647dd1ad66ef0b1 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 3 Mar 2026 11:36:30 +0100 Subject: [PATCH 475/593] Improved 'do SAS validation again' button UI --- .../call_media_encryption_stats_bottom_sheet.xml | 7 +++++-- .../layout/call_media_encryption_stats_bottom_sheet.xml | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout-land/call_media_encryption_stats_bottom_sheet.xml b/app/src/main/res/layout-land/call_media_encryption_stats_bottom_sheet.xml index 380f4528af..31004917cd 100644 --- a/app/src/main/res/layout-land/call_media_encryption_stats_bottom_sheet.xml +++ b/app/src/main/res/layout-land/call_media_encryption_stats_bottom_sheet.xml @@ -138,12 +138,15 @@ + app:layout_columnSpan="2" + app:layout_gravity="center_horizontal"/> diff --git a/app/src/main/res/layout/call_media_encryption_stats_bottom_sheet.xml b/app/src/main/res/layout/call_media_encryption_stats_bottom_sheet.xml index b841202dd9..dc9321dc5c 100644 --- a/app/src/main/res/layout/call_media_encryption_stats_bottom_sheet.xml +++ b/app/src/main/res/layout/call_media_encryption_stats_bottom_sheet.xml @@ -131,10 +131,13 @@ From a2b2b540e8126c8170cc4a4f7da39262dda81989 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 3 Mar 2026 17:20:11 +0100 Subject: [PATCH 476/593] Removed 'disable video' setting --- .../settings/viewmodel/SettingsViewModel.kt | 14 +-------- .../settings_advanced_calls_fragment.xml | 5 +-- app/src/main/res/layout/settings_calls.xml | 31 +------------------ app/src/main/res/layout/settings_network.xml | 2 +- 4 files changed, 4 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index affdf62981..08f498ba22 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -84,7 +84,6 @@ class SettingsViewModel val adaptiveRateControlEnabled = MutableLiveData() - val videoEnabled = MutableLiveData() val videoFecEnabled = MutableLiveData() val isVibrationAvailable = MutableLiveData() @@ -329,8 +328,7 @@ class SettingsViewModel } adaptiveRateControlEnabled.postValue(core.isAdaptiveRateControlEnabled) - - videoEnabled.postValue(core.isVideoEnabled) + videoFecEnabled.postValue(core.isFecEnabled) vibrateDuringIncomingCall.postValue(core.isVibrationOnIncomingCallEnabled) autoRecordCalls.postValue(corePreferences.automaticallyStartCallRecording) @@ -474,16 +472,6 @@ class SettingsViewModel } } - @UiThread - fun toggleEnableVideo() { - val newValue = videoEnabled.value == false - coreContext.postOnCoreThread { core -> - core.isVideoCaptureEnabled = newValue - core.isVideoDisplayEnabled = newValue - videoEnabled.postValue(newValue) - } - } - @UiThread fun toggleEnableVideoFec() { val newValue = videoFecEnabled.value == false diff --git a/app/src/main/res/layout/settings_advanced_calls_fragment.xml b/app/src/main/res/layout/settings_advanced_calls_fragment.xml index ff118f73ab..0a59850e21 100644 --- a/app/src/main/res/layout/settings_advanced_calls_fragment.xml +++ b/app/src/main/res/layout/settings_advanced_calls_fragment.xml @@ -83,7 +83,6 @@ android:maxLines="2" android:ellipsize="end" android:labelFor="@id/enable_fec_switch" - android:visibility="@{viewModel.videoEnabled ? View.VISIBLE : View.GONE}" app:layout_constraintTop_toTopOf="@id/enable_fec_switch" app:layout_constraintBottom_toBottomOf="@id/enable_fec_switch" app:layout_constraintStart_toStartOf="parent" @@ -97,9 +96,7 @@ android:layout_height="wrap_content" android:layout_marginTop="20dp" android:layout_marginEnd="16dp" - android:visibility="@{viewModel.videoEnabled ? View.VISIBLE : View.GONE}" - android:enabled="@{viewModel.videoEnabled}" - android:checked="@{viewModel.videoEnabled && viewModel.videoFecEnabled}" + android:checked="@{viewModel.videoFecEnabled}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/layout/settings_calls.xml b/app/src/main/res/layout/settings_calls.xml index 81dfde71d1..64d33cd49f 100644 --- a/app/src/main/res/layout/settings_calls.xml +++ b/app/src/main/res/layout/settings_calls.xml @@ -124,35 +124,6 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/calibrate_echo_canceller" /> - - - - + app:layout_constraintTop_toBottomOf="@id/adaptive_rate_control_switch" /> Date: Tue, 3 Mar 2026 17:50:40 +0100 Subject: [PATCH 477/593] Allow long press on digits in numpad to produce a long sound instead of a short one no matter how long the button was pressed --- .../java/org/linphone/core/CoreContext.kt | 19 ------ .../ui/call/fragment/ActiveCallFragment.kt | 6 ++ .../ui/call/viewmodel/CurrentCallViewModel.kt | 5 ++ .../ui/main/history/model/NumpadModel.kt | 61 +++++++++++++++++-- .../settings/viewmodel/SettingsViewModel.kt | 2 +- .../org/linphone/utils/DataBindingUtils.kt | 19 ++++++ .../layout-land/call_numpad_bottom_sheet.xml | 1 + .../res/layout/call_numpad_bottom_sheet.xml | 1 + app/src/main/res/layout/call_numpad_digit.xml | 2 + .../layout/call_numpad_digit_with_letters.xml | 2 + .../layout/call_numpad_digit_with_plus.xml | 2 + .../call_numpad_digit_with_voicemail.xml | 2 + 12 files changed, 96 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 420f5d0cfc..8f2468e8ab 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -32,8 +32,6 @@ import android.os.Handler import android.os.HandlerThread import android.os.Looper import android.os.PowerManager -import android.provider.Settings -import android.provider.Settings.SettingNotFoundException import androidx.annotation.AnyThread import androidx.annotation.UiThread import androidx.annotation.WorkerThread @@ -1111,23 +1109,6 @@ class CoreContext keepAliveServiceStarted = false } - @WorkerThread - fun playDtmf(character: Char, duration: Int = 200, ignoreSystemPolicy: Boolean = false) { - try { - if (ignoreSystemPolicy || Settings.System.getInt( - context.contentResolver, - Settings.System.DTMF_TONE_WHEN_DIALING - ) != 0 - ) { - core.playDtmf(character, duration) - } else { - Log.w("$TAG Numpad DTMF tones are disabled in system settings, not playing them") - } - } catch (snfe: SettingNotFoundException) { - Log.e("$TAG DTMF_TONE_WHEN_DIALING system setting not found: $snfe") - } - } - @WorkerThread fun computeUserAgent() { val savedDeviceName = corePreferences.deviceName diff --git a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt index dc1933d078..9c8e10ba20 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt @@ -304,6 +304,12 @@ class ActiveCallFragment : GenericCallFragment() { } } + callViewModel.clearPressedDtmfBarEvent.observe(viewLifecycleOwner) { + it.consume { + binding.callNumpad.digitsHistory.setText("") + } + } + callViewModel.appendDigitToSearchBarEvent.observe(viewLifecycleOwner) { it.consume { digit -> binding.callNumpad.digitsHistory.addCharacterAtPosition(digit) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 9785cc7384..9b5234a90e 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -260,6 +260,10 @@ class CurrentCallViewModel MutableLiveData() } + val clearPressedDtmfBarEvent: MutableLiveData> by lazy { + MutableLiveData() + } + // Sliding answer/decline button val isScreenLocked = MutableLiveData() @@ -577,6 +581,7 @@ class CurrentCallViewModel { // OnBlindTransferClicked }, { // OnClearInput + clearPressedDtmfBarEvent.value = Event(true) } ) diff --git a/app/src/main/java/org/linphone/ui/main/history/model/NumpadModel.kt b/app/src/main/java/org/linphone/ui/main/history/model/NumpadModel.kt index 9a74695c99..5eff23c66f 100644 --- a/app/src/main/java/org/linphone/ui/main/history/model/NumpadModel.kt +++ b/app/src/main/java/org/linphone/ui/main/history/model/NumpadModel.kt @@ -19,12 +19,16 @@ */ package org.linphone.ui.main.history.model +import android.provider.Settings +import android.provider.Settings.SettingNotFoundException +import android.view.View import androidx.annotation.UiThread import androidx.lifecycle.MutableLiveData import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.core.tools.Log import org.linphone.utils.LinphoneUtils +import org.linphone.utils.TouchListener open class NumpadModel @UiThread @@ -47,7 +51,38 @@ open class NumpadModel val showLetters = MutableLiveData() + val systemPolicyAllowsDtmf: Boolean + + val touchListener = object : TouchListener { + override fun onPressed(view: View): Boolean { + Log.i("$TAG Numpad digit [${view.tag}] pressed") + startPlayingDtmf(view.tag.toString()) + return false + } + + override fun onReleased(view: View): Boolean { + Log.i("$TAG Numpad digit [${view.tag}] released") + stopPlayingDtmf(view.tag.toString()) + return false + } + } + init { + var dtmfAllowed = false + try { + dtmfAllowed = Settings.System.getInt( + coreContext.context.contentResolver, + Settings.System.DTMF_TONE_WHEN_DIALING + ) != 0 + if (!dtmfAllowed) { + Log.w("$TAG Numpad DTMF tones are disabled in system settings, not playing them") + } + } catch (snfe: SettingNotFoundException) { + Log.e("$TAG DTMF_TONE_WHEN_DIALING system setting not found: $snfe") + dtmfAllowed = false + } + systemPolicyAllowsDtmf = dtmfAllowed + coreContext.postOnCoreThread { showLetters.postValue(corePreferences.showLettersOnDialpad) @@ -61,12 +96,6 @@ open class NumpadModel fun onDigitClicked(value: String) { Log.i("$TAG Clicked on digit [$value]") onDigitClicked.invoke(value) - - if (value.isNotEmpty()) { - coreContext.postOnCoreThread { - coreContext.playDtmf(value[0], ignoreSystemPolicy = inCallNumpad) - } - } } @UiThread @@ -113,4 +142,24 @@ open class NumpadModel Log.i("$TAG Transferring call") onTransferCallClicked.invoke() } + + @UiThread + private fun startPlayingDtmf(dtmf: String) { + if (dtmf.isEmpty() || (!inCallNumpad && !systemPolicyAllowsDtmf)) return + + coreContext.postOnCoreThread { core -> + Log.i("$TAG Start playing DTMF [$dtmf]") + core.playDtmf(dtmf[0], -1) + } + } + + @UiThread + private fun stopPlayingDtmf(dtmf: String) { + if (dtmf.isEmpty() || (!inCallNumpad && !systemPolicyAllowsDtmf)) return + + coreContext.postOnCoreThread { core -> + Log.i("$TAG Stop playing DTMF [$dtmf]") + core.stopDtmf() + } + } } diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 08f498ba22..6a85ee4ecb 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -328,7 +328,7 @@ class SettingsViewModel } adaptiveRateControlEnabled.postValue(core.isAdaptiveRateControlEnabled) - + videoFecEnabled.postValue(core.isFecEnabled) vibrateDuringIncomingCall.postValue(core.isVibrationOnIncomingCallEnabled) autoRecordCalls.postValue(corePreferences.automaticallyStartCallRecording) diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 007053b806..5cc171bcb7 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -26,6 +26,7 @@ import android.graphics.drawable.AnimatedVectorDrawable import android.text.Editable import android.text.TextWatcher import android.view.LayoutInflater +import android.view.MotionEvent import android.view.TextureView import android.view.View import android.view.ViewGroup @@ -652,3 +653,21 @@ fun EmojiPickerView.setEmojiPickedListener(listener: EmojiPickedListener) { interface EmojiPickedListener { fun onEmojiPicked(item: EmojiViewItem) } + +@SuppressLint("ClickableViewAccessibility") +@BindingAdapter("onTouchListener") +fun View.setTouchListener(listener: TouchListener) { + setOnTouchListener { view, event -> + return@setOnTouchListener when (event.action) { + MotionEvent.ACTION_DOWN -> listener.onPressed(view) + MotionEvent.ACTION_UP -> listener.onReleased(view) + else -> false + } + } +} + +interface TouchListener { + fun onPressed(view: View): Boolean + + fun onReleased(view: View): Boolean +} diff --git a/app/src/main/res/layout-land/call_numpad_bottom_sheet.xml b/app/src/main/res/layout-land/call_numpad_bottom_sheet.xml index d19fd42575..75dc310e96 100644 --- a/app/src/main/res/layout-land/call_numpad_bottom_sheet.xml +++ b/app/src/main/res/layout-land/call_numpad_bottom_sheet.xml @@ -63,6 +63,7 @@ diff --git a/app/src/main/res/layout/call_numpad_digit_with_letters.xml b/app/src/main/res/layout/call_numpad_digit_with_letters.xml index e56ecd4488..92ba4b2f14 100644 --- a/app/src/main/res/layout/call_numpad_digit_with_letters.xml +++ b/app/src/main/res/layout/call_numpad_digit_with_letters.xml @@ -18,6 +18,8 @@ diff --git a/app/src/main/res/layout/call_numpad_digit_with_plus.xml b/app/src/main/res/layout/call_numpad_digit_with_plus.xml index ea9ff20a8c..099213dc97 100644 --- a/app/src/main/res/layout/call_numpad_digit_with_plus.xml +++ b/app/src/main/res/layout/call_numpad_digit_with_plus.xml @@ -16,6 +16,8 @@ diff --git a/app/src/main/res/layout/call_numpad_digit_with_voicemail.xml b/app/src/main/res/layout/call_numpad_digit_with_voicemail.xml index 8b19331024..c7792a7e62 100644 --- a/app/src/main/res/layout/call_numpad_digit_with_voicemail.xml +++ b/app/src/main/res/layout/call_numpad_digit_with_voicemail.xml @@ -18,6 +18,8 @@ From c747bc76c1800f8cfed5b8092a94454cc037691a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 4 Mar 2026 10:54:25 +0100 Subject: [PATCH 478/593] Bumped AGP to 9.1 and gradle to 9.3.1 --- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 77c5e02204..7a380384c0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "9.0.1" +agp = "9.1.0" kotlin = "2.3.10" gmsGoogleServices = "4.4.4" firebaseCrashlytics = "3.0.6" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index dd20b755c9..98556d5d93 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Jun 22 12:11:25 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From e14365be170a2a6d8a1c9007c1e7212227e3dc07 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 4 Mar 2026 11:21:29 +0100 Subject: [PATCH 479/593] Added a setting to show past meetings (now hidden by default) --- CHANGELOG.md | 1 + .../java/org/linphone/core/CorePreferences.kt | 7 +++++ .../viewmodel/MeetingsListViewModel.kt | 12 +++++++ .../settings/fragment/SettingsFragment.kt | 6 ++++ .../settings/viewmodel/SettingsViewModel.kt | 16 ++++++++++ app/src/main/res/layout/settings_meetings.xml | 31 ++++++++++++++++++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 8 files changed, 74 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1f22f4e7a..ed7014eaee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Group changes to describe their impact on the project, as follows: - one to hide contacts that have neither a SIP address nor a phone number - one to let app auto-answer call with video sending already enabled - one to let edit native contacts Linphone copy in-app instead of opening native addressbook third party app + - one to show past meetings (they are now hidden by default) - Added a vu meter for recording & playback volumes (must be enabled in developer settings) - Added support for HDMI audio devices - Added video preview during in-call conversation diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index a7c7deffe4..4fa3a5e0fe 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -230,6 +230,13 @@ class CorePreferences config.setBool("app", "create_e2e_encrypted_conferences", value) } + @get:AnyThread @set:WorkerThread + var showPastMeetings: Boolean + get() = config.getBool("ui", "show_past_meetings", false) + set(value) { + config.setBool("ui", "show_past_meetings", value) + } + // Contacts related @get:AnyThread @set:WorkerThread diff --git a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt index d82254146c..00ac002a8a 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/viewmodel/MeetingsListViewModel.kt @@ -23,6 +23,7 @@ import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.core.Account import org.linphone.core.AccountListenerStub import org.linphone.core.ConferenceInfo @@ -114,6 +115,9 @@ class MeetingsListViewModel fetchInProgress.postValue(true) } + val showPastMeetings = corePreferences.showPastMeetings + val nowInSecs = System.currentTimeMillis() / 1000 + val sortedSource = source.toList().sortedBy { it.dateTime } @@ -135,6 +139,14 @@ class MeetingsListViewModel ) continue } // This isn't a scheduled conference, don't display it + + if (!showPastMeetings && (info.dateTime + (info.duration * 60) < nowInSecs)) { + Log.d( + "$TAG Skipping conference info [${info.subject}] with uri [${info.uri?.asStringUriOnly()}] because it's in the past" + ) + continue + } + val add = if (filter.isNotEmpty()) { val organizerCheck = info.organizer?.asStringUriOnly()?.contains( filter, diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt index e862a7602b..2bb045f7bf 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt @@ -325,6 +325,12 @@ class SettingsFragment : GenericMainFragment() { binding.tunnelSettings.tunnelModeSpinner.setSelection(index) } + viewModel.forceRefreshMeetingsListEvent.observe(viewLifecycleOwner) { + it.consume { + sharedViewModel.forceRefreshMeetingsListEvent.postValue(Event(true)) + } + } + binding.setTurnOnVfsClickListener { showConfirmVfsDialog() } diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 6a85ee4ecb..fc8bd0be0b 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -140,6 +140,11 @@ class SettingsViewModel // Meetings settings val showMeetingsSettings = MutableLiveData() + val showPastMeetings = MutableLiveData() + val forceRefreshMeetingsListEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + val defaultLayout = MutableLiveData() val availableLayoutsNames = arrayListOf( AppUtils.getString(R.string.settings_meetings_layout_active_speaker_label), @@ -348,6 +353,7 @@ class SettingsViewModel hideEmptyContacts.postValue(corePreferences.hideContactsWithoutPhoneNumberOrSipAddress) presenceSubscribe.postValue(core.isFriendListSubscriptionEnabled) + showPastMeetings.postValue(corePreferences.showPastMeetings) defaultLayout.postValue(core.defaultConferenceLayout.toInt()) autoShowDialpad.postValue(corePreferences.automaticallyShowDialpad) @@ -663,6 +669,16 @@ class SettingsViewModel expandMeetings.value = expandMeetings.value == false } + @UiThread + fun toggleShowPastMeetings() { + val newValue = showPastMeetings.value == false + coreContext.postOnCoreThread { + corePreferences.showPastMeetings = newValue + showPastMeetings.postValue(newValue) + forceRefreshMeetingsListEvent.postValue(Event(true)) + } + } + @UiThread fun setDefaultLayout(layoutValue: Int) { coreContext.postOnCoreThread { core -> diff --git a/app/src/main/res/layout/settings_meetings.xml b/app/src/main/res/layout/settings_meetings.xml index 302661e2bb..e9697f846d 100644 --- a/app/src/main/res/layout/settings_meetings.xml +++ b/app/src/main/res/layout/settings_meetings.xml @@ -16,6 +16,35 @@ android:paddingBottom="20dp" android:background="@drawable/shape_squircle_white_background"> + + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 455ece6025..9ca7d36f2c 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -265,6 +265,7 @@ Une erreur s\'est produite, la configuration LDAP n\'a pas été sauvegardée ! Tous les champs doivent être remplis Réunions + Afficher les réunions passées Disposition par défaut Intervenant actif Mosaïque diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b6e1a08b5a..14da32a7cf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -307,6 +307,7 @@ A error occurred, LDAP server not saved! All fields must be filled Meetings + Show past meetings Default layout Active speaker Mosaic From a22f4ad88c3b735baa23367c283db259a0d9ae0b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 5 Mar 2026 09:58:04 +0100 Subject: [PATCH 480/593] Prevent keyboard for correcting user input for SIP address text fields --- app/src/main/res/layout/account_nat_policy_settings.xml | 2 +- app/src/main/res/layout/call_transfer_fragment.xml | 2 +- app/src/main/res/layout/chat_message_forward_fragment.xml | 2 +- app/src/main/res/layout/generic_add_participants_fragment.xml | 2 +- app/src/main/res/layout/start_call_fragment.xml | 2 +- app/src/main/res/layout/start_chat_fragment.xml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/layout/account_nat_policy_settings.xml b/app/src/main/res/layout/account_nat_policy_settings.xml index 21c0946d4d..e6ab58bc56 100644 --- a/app/src/main/res/layout/account_nat_policy_settings.xml +++ b/app/src/main/res/layout/account_nat_policy_settings.xml @@ -133,7 +133,7 @@ android:paddingStart="20dp" android:paddingEnd="20dp" android:text="@={viewModel.turnUsername}" - android:inputType="text|textPersonName" + android:inputType="text|textUri|textNoSuggestions" android:hint="@string/account_settings_turn_username_title" app:layout_constraintHorizontal_bias="0" app:layout_constraintWidth_max="@dimen/text_input_max_width" diff --git a/app/src/main/res/layout/call_transfer_fragment.xml b/app/src/main/res/layout/call_transfer_fragment.xml index aa4d1ba1a3..5062ab2d80 100644 --- a/app/src/main/res/layout/call_transfer_fragment.xml +++ b/app/src/main/res/layout/call_transfer_fragment.xml @@ -132,7 +132,7 @@ android:drawablePadding="10dp" android:drawableTint="?attr/color_main2_600" android:hint="@string/history_call_start_search_bar_filter_hint" - android:inputType="text|textPersonName|textNoSuggestions" + android:inputType="text|textUri|textNoSuggestions" android:paddingStart="15dp" android:paddingTop="10dp" android:paddingEnd="45dp" diff --git a/app/src/main/res/layout/chat_message_forward_fragment.xml b/app/src/main/res/layout/chat_message_forward_fragment.xml index b1d2459cc7..73d9e01d40 100644 --- a/app/src/main/res/layout/chat_message_forward_fragment.xml +++ b/app/src/main/res/layout/chat_message_forward_fragment.xml @@ -81,7 +81,7 @@ android:drawablePadding="10dp" android:drawableTint="?attr/color_main2_600" android:hint="@string/new_conversation_search_bar_filter_hint" - android:inputType="text|textPersonName|textNoSuggestions" + android:inputType="text|textUri|textNoSuggestions" android:paddingStart="15dp" android:paddingTop="10dp" android:paddingEnd="15dp" diff --git a/app/src/main/res/layout/generic_add_participants_fragment.xml b/app/src/main/res/layout/generic_add_participants_fragment.xml index 6a4fc69a0a..3fcdf3bf4b 100644 --- a/app/src/main/res/layout/generic_add_participants_fragment.xml +++ b/app/src/main/res/layout/generic_add_participants_fragment.xml @@ -116,7 +116,7 @@ android:drawablePadding="10dp" android:drawableTint="?attr/color_main2_600" android:hint="@string/new_conversation_search_bar_filter_hint" - android:inputType="text|textPersonName|textNoSuggestions" + android:inputType="text|textUri|textNoSuggestions" android:paddingStart="15dp" android:paddingTop="10dp" android:paddingEnd="15dp" diff --git a/app/src/main/res/layout/start_call_fragment.xml b/app/src/main/res/layout/start_call_fragment.xml index a6df330cff..8809708571 100644 --- a/app/src/main/res/layout/start_call_fragment.xml +++ b/app/src/main/res/layout/start_call_fragment.xml @@ -125,7 +125,7 @@ android:drawablePadding="10dp" android:drawableTint="?attr/color_main2_600" android:hint="@string/history_call_start_search_bar_filter_hint" - android:inputType="text|textPersonName|textNoSuggestions" + android:inputType="text|textUri|textNoSuggestions" android:paddingStart="15dp" android:paddingTop="10dp" android:paddingEnd="45dp" diff --git a/app/src/main/res/layout/start_chat_fragment.xml b/app/src/main/res/layout/start_chat_fragment.xml index 3249c2a0fd..616672d3eb 100644 --- a/app/src/main/res/layout/start_chat_fragment.xml +++ b/app/src/main/res/layout/start_chat_fragment.xml @@ -121,7 +121,7 @@ android:drawablePadding="10dp" android:drawableTint="?attr/color_main2_600" android:hint="@string/new_conversation_search_bar_filter_hint" - android:inputType="text|textPersonName|textNoSuggestions" + android:inputType="text|textUri|textNoSuggestions" android:paddingStart="15dp" android:paddingTop="10dp" android:paddingEnd="15dp" From b16b42ce55bce9026eb91bc91e3d4b02af1a80fa Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 5 Mar 2026 11:34:59 +0100 Subject: [PATCH 481/593] Added back hide message content in Android notification setting --- CHANGELOG.md | 1 + .../java/org/linphone/core/CorePreferences.kt | 7 +++++ .../notifications/NotificationsManager.kt | 7 ++++- .../settings/viewmodel/SettingsViewModel.kt | 12 ++++++++ app/src/main/res/layout/settings_chat.xml | 29 +++++++++++++++++++ app/src/main/res/values-fr/strings.xml | 2 ++ app/src/main/res/values/strings.xml | 2 ++ 7 files changed, 59 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed7014eaee..597f83f819 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Group changes to describe their impact on the project, as follows: - one to let app auto-answer call with video sending already enabled - one to let edit native contacts Linphone copy in-app instead of opening native addressbook third party app - one to show past meetings (they are now hidden by default) + - one to hide received message content in android notification - Added a vu meter for recording & playback volumes (must be enabled in developer settings) - Added support for HDMI audio devices - Added video preview during in-call conversation diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 4fa3a5e0fe..7905b3a0c8 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -221,6 +221,13 @@ class CorePreferences config.setBool("app", "make_downloaded_images_public_in_gallery", value) } + @get:AnyThread @set:WorkerThread + var hideChatMessageContentInNotification: Boolean + get() = config.getBool("ui", "display_notification_content", false) + set(value) { + config.setBool("ui", "display_notification_content", value) + } + // Conference related @get:AnyThread @set:WorkerThread diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 6bde597276..1eec6f219e 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -1600,8 +1600,13 @@ class NotificationsManager } val senderPerson = if (message.isOutgoing) null else person // Use null for ourselves + val text = if (corePreferences.hideChatMessageContentInNotification) { + AppUtils.getString(R.string.notification_chat_message_hidden_content) + } else { + message.message + } val tmp = NotificationCompat.MessagingStyle.Message( - message.message, + text, message.time, senderPerson ) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index fc8bd0be0b..12d139c7fd 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -102,6 +102,8 @@ class SettingsViewModel val markAsReadWhenDismissingNotification = MutableLiveData() + val hideMessageContentInNotification = MutableLiveData() + // Contacts settings val showContactsSettings = MutableLiveData() @@ -347,6 +349,7 @@ class SettingsViewModel markAsReadWhenDismissingNotification.postValue( corePreferences.markConversationAsReadWhenDismissingMessageNotification ) + hideMessageContentInNotification.postValue(corePreferences.hideChatMessageContentInNotification) sortContactsBy.postValue(if (corePreferences.sortContactsByFirstName) 0 else 1) editNativeContactsInLinphone.postValue(corePreferences.editNativeContactsInLinphone) @@ -573,6 +576,15 @@ class SettingsViewModel } } + @UiThread + fun toggleHideMessageContentInNotification() { + val newValue = hideMessageContentInNotification.value == false + coreContext.postOnCoreThread { + corePreferences.hideChatMessageContentInNotification = newValue + hideMessageContentInNotification.postValue(newValue) + } + } + @UiThread fun toggleContactsExpand() { expandContacts.value = expandContacts.value == false diff --git a/app/src/main/res/layout/settings_chat.xml b/app/src/main/res/layout/settings_chat.xml index f02269ef6b..3534e5e893 100644 --- a/app/src/main/res/layout/settings_chat.xml +++ b/app/src/main/res/layout/settings_chat.xml @@ -104,6 +104,35 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/auto_export_media_to_native_gallery_switch" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 9ca7d36f2c..1beca0ede7 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -78,6 +78,7 @@ Ouvrez &appName; pour rafraîchir la connexion Votre correspondant ne vous entend pas ! Cliquez sur cette notification pour corriger le problème + <contenu masqué> Bienvenue @@ -227,6 +228,7 @@ Télécharger automatiquement les fichiers Rendre visible dans la galerie les médias téléchargés Marquer la conversation comme lue lorsqu\'une notification de message est supprimée + Masquer le contenu du message dans la notification Android Contacts Trier les contacts par Editer les contacts natifs dans &appName; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 14da32a7cf..54bef35ef5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -120,6 +120,7 @@ Open &appName; to refresh the registration Your correspondent does not hear you! Click on this notification to fix it + <redacted> Welcome @@ -269,6 +270,7 @@ Auto-download files Make downloaded media public Mark conversation as read when dismissing message notification + Do not show message content in Android notification Contacts Sort contacts by Use &appName; contact editor for native contacts From 15603e79335b3b7b4a824cbf28c8716839338336 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 5 Mar 2026 12:10:12 +0100 Subject: [PATCH 482/593] Fixed issue in previous commit --- app/src/main/java/org/linphone/core/CorePreferences.kt | 4 ++-- .../java/org/linphone/notifications/NotificationsManager.kt | 6 +++--- .../ui/main/settings/viewmodel/SettingsViewModel.kt | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 7905b3a0c8..8d90a06961 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -222,8 +222,8 @@ class CorePreferences } @get:AnyThread @set:WorkerThread - var hideChatMessageContentInNotification: Boolean - get() = config.getBool("ui", "display_notification_content", false) + var showChatMessageContentInNotification: Boolean + get() = config.getBool("ui", "display_notification_content", true) set(value) { config.setBool("ui", "display_notification_content", value) } diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 1eec6f219e..8b04b41086 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -1600,10 +1600,10 @@ class NotificationsManager } val senderPerson = if (message.isOutgoing) null else person // Use null for ourselves - val text = if (corePreferences.hideChatMessageContentInNotification) { - AppUtils.getString(R.string.notification_chat_message_hidden_content) - } else { + val text = if (corePreferences.showChatMessageContentInNotification) { message.message + } else { + AppUtils.getString(R.string.notification_chat_message_hidden_content) } val tmp = NotificationCompat.MessagingStyle.Message( text, diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 12d139c7fd..4aa6cf27eb 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -349,7 +349,7 @@ class SettingsViewModel markAsReadWhenDismissingNotification.postValue( corePreferences.markConversationAsReadWhenDismissingMessageNotification ) - hideMessageContentInNotification.postValue(corePreferences.hideChatMessageContentInNotification) + hideMessageContentInNotification.postValue(!corePreferences.showChatMessageContentInNotification) sortContactsBy.postValue(if (corePreferences.sortContactsByFirstName) 0 else 1) editNativeContactsInLinphone.postValue(corePreferences.editNativeContactsInLinphone) @@ -580,7 +580,7 @@ class SettingsViewModel fun toggleHideMessageContentInNotification() { val newValue = hideMessageContentInNotification.value == false coreContext.postOnCoreThread { - corePreferences.hideChatMessageContentInNotification = newValue + corePreferences.showChatMessageContentInNotification = !newValue hideMessageContentInNotification.postValue(newValue) } } From d6af726227386211eaac03cbd4cd49fff8a78099 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 5 Mar 2026 13:32:13 +0000 Subject: [PATCH 483/593] Updated translations from Weblate --- app/src/main/res/values-ca/strings.xml | 5 +++++ app/src/main/res/values-cs/strings.xml | 12 ++++++++++++ app/src/main/res/values-de/strings.xml | 2 ++ 3 files changed, 19 insertions(+) diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index ffa3a0c933..8fd6d6c643 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -6,4 +6,9 @@ %s dies %s dies + Prem a la notificació per arregar-ho + Benvingut + Assegurada + No t\'escolten! + Un aplicatiu de comunicació segur, de codi obert i en francès. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 0d4fb36152..91cb60b1e4 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -903,4 +903,16 @@ Rozsah IP: %s Zrušit úpravu zprávy Zavřít menu + Volaný vás neslyší! + Žádné osiřelé autentizační údaje nebyly nalezeny + + %s osiřelý autentizační údaj odstraněn + %s osiřelé autentizační údaje byly odstraněny + %s osiřelých autentizačních údajů bylo odstraněno + + Klikněte na toto oznámení a opravte to + Stav připojení + Vymazat autentizační údaje, které již nejsou spojeny s žádným účtem + Výběr vyzváněcích tónů není k dispozici! + Koncově šifrováno diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d42cc14f5e..ed6356ae1c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -904,4 +904,6 @@ %s verwaiste Anmeldeinformation entfernt %s verwaiste Anmeldeinformationen entfernt + Klicken Sie auf diese Benachrichtigung, um das Problem zu beheben + Ihr Gesprächspartner hört Sie nicht! From 18dbe4356a4236ce1dda91a84aed914b6e9fda49 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 5 Mar 2026 16:05:11 +0100 Subject: [PATCH 484/593] Improve account creation when SMS can't be sent --- .../ui/assistant/fragment/RegisterFragment.kt | 67 ++++++++--- .../viewmodel/AccountCreationViewModel.kt | 15 ++- .../java/org/linphone/utils/DialogUtils.kt | 17 +++ ..._phone_number_validation_not_available.xml | 106 ++++++++++++++++++ app/src/main/res/values-fr/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 6 files changed, 193 insertions(+), 18 deletions(-) create mode 100644 app/src/main/res/layout/dialog_assistant_create_account_phone_number_validation_not_available.xml diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt index 6790a77197..c2d72588ce 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt @@ -100,23 +100,7 @@ class RegisterFragment : GenericFragment() { } binding.setOpenSubscribeWebPageClickListener { - val url = getString(R.string.web_platform_register_email_url) - try { - val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) - startActivity(browserIntent) - } catch (ise: IllegalStateException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" - ) - } catch (anfe: ActivityNotFoundException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" - ) - } catch (e: Exception) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" - ) - } + openSubscribeOnlineWebpage() } binding.username.addTextChangedListener(object : TextWatcher { @@ -152,6 +136,12 @@ class RegisterFragment : GenericFragment() { } } + viewModel.accountCantBeCreatedBySmsEvent.observe(viewLifecycleOwner) { + it.consume { + showPhoneNumberValidationNotAvailableDialog() + } + } + viewModel.goToSmsCodeConfirmationViewEvent.observe(viewLifecycleOwner) { it.consume { Log.i("$TAG Going to SMS code confirmation fragment") @@ -224,4 +214,47 @@ class RegisterFragment : GenericFragment() { dialog.show() } + + private fun openSubscribeOnlineWebpage() { + val url = getString(R.string.web_platform_register_email_url) + try { + val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) + startActivity(browserIntent) + } catch (ise: IllegalStateException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" + ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" + ) + } catch (e: Exception) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" + ) + } + } + + private fun showPhoneNumberValidationNotAvailableDialog() { + val model = ConfirmationDialogModel() + val dialog = DialogUtils.getAccountCreationPhoneNumberValidationNotAvailableDialog( + requireActivity(), + model + ) + + model.dismissEvent.observe(viewLifecycleOwner) { + it.consume { + dialog.dismiss() + } + } + + model.confirmEvent.observe(viewLifecycleOwner) { + it.consume { + openSubscribeOnlineWebpage() + dialog.dismiss() + } + } + + dialog.show() + } } diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt index 139145f212..19ebe95294 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt @@ -108,6 +108,10 @@ class AccountCreationViewModel MutableLiveData() } + val accountCantBeCreatedBySmsEvent: MutableLiveData> by lazy { + MutableLiveData() + } + private var waitingForFlexiApiPushToken = false private var waitForPushJob: Job? = null @@ -166,7 +170,11 @@ class AccountCreationViewModel operationInProgress.postValue(false) if (!errorMessage.isNullOrEmpty()) { - showFormattedRedToast(errorMessage, R.drawable.warning_circle) + if (request.type == AccountManagerServicesRequest.Type.SendPhoneNumberLinkingCodeBySms && statusCode == 422) { + // Do not show error message sent by the account management platform for this specific scenario + } else { + showFormattedRedToast(errorMessage, R.drawable.warning_circle) + } } for (parameter in parameterErrors?.keys.orEmpty()) { @@ -186,6 +194,7 @@ class AccountCreationViewModel waitForPushJob?.cancel() } AccountManagerServicesRequest.Type.SendPhoneNumberLinkingCodeBySms -> { + Log.e("$TAG Error sending SMS code, clearing auth info & account") val authInfo = accountCreatedAuthInfo if (authInfo != null) { coreContext.core.removeAuthInfo(authInfo) @@ -194,6 +203,10 @@ class AccountCreationViewModel if (account != null) { coreContext.core.removeAccount(account) } + + if (statusCode == 422) { + accountCantBeCreatedBySmsEvent.postValue(Event(true)) + } } else -> { } diff --git a/app/src/main/java/org/linphone/utils/DialogUtils.kt b/app/src/main/java/org/linphone/utils/DialogUtils.kt index ffa5a40fa7..5c4fa287bc 100644 --- a/app/src/main/java/org/linphone/utils/DialogUtils.kt +++ b/app/src/main/java/org/linphone/utils/DialogUtils.kt @@ -66,6 +66,7 @@ import org.linphone.ui.main.contacts.model.ContactTrustDialogModel import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel import org.linphone.ui.main.model.GroupSetOrEditSubjectDialogModel import androidx.core.graphics.drawable.toDrawable +import org.linphone.databinding.DialogAssistantCreateAccountPhoneNumberValidationNotAvailableBinding import org.linphone.databinding.DialogDeleteChatMessageBinding import org.linphone.databinding.DialogManageAccountOutboundProxyHelpBinding import org.linphone.ui.main.chat.model.MessageDeleteDialogModel @@ -105,6 +106,22 @@ class DialogUtils { return getDialog(context, binding) } + @UiThread + fun getAccountCreationPhoneNumberValidationNotAvailableDialog( + context: Context, + viewModel: ConfirmationDialogModel + ): Dialog { + val binding: DialogAssistantCreateAccountPhoneNumberValidationNotAvailableBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.dialog_assistant_create_account_phone_number_validation_not_available, + null, + false + ) + binding.viewModel = viewModel + + return getDialog(context, binding) + } + @UiThread fun getAccountInternationalPrefixHelpDialog(context: Context): Dialog { val binding: DialogManageAccountInternationalPrefixHelpBinding = DataBindingUtil.inflate( diff --git a/app/src/main/res/layout/dialog_assistant_create_account_phone_number_validation_not_available.xml b/app/src/main/res/layout/dialog_assistant_create_account_phone_number_validation_not_available.xml new file mode 100644 index 0000000000..059010a2a5 --- /dev/null +++ b/app/src/main/res/layout/dialog_assistant_create_account_phone_number_validation_not_available.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1beca0ede7..b2d20ec06b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -108,6 +108,9 @@ En continuant, vous acceptez nos %1$s et %2$s. Confirmez votre numéro de téléphone Êtes-vous sûr que le %s est votre numéro de téléphone ? + Inscription par téléphone indisponible + L\'inscription par numéro de téléphone n\'est pas disponible, veuillez utiliser l\'inscription par adresse email. + S\'inscrire avec un email Connexion Scanner un QR code Ce QR code est invalide ! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 54bef35ef5..acb794950f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -150,6 +150,9 @@ By continuing, you accept our %1$s and %2$s. Confirm phone number Are you sure your phone number is %s? + Phone number validation not available + Phone number validation is not available, please use email account creation process. + Register with an email Login Scan QR code Invalid QR code! From 29944b9aa81913f62ca4fc633978ec86d9ccf9c5 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 9 Mar 2026 14:53:10 +0100 Subject: [PATCH 485/593] Force left to right direction for numpad layouts --- app/src/main/res/layout-land/call_numpad_bottom_sheet.xml | 1 + app/src/main/res/layout/call_numpad_bottom_sheet.xml | 1 + app/src/main/res/layout/start_call_numpad_bottom_sheet.xml | 1 + 3 files changed, 3 insertions(+) diff --git a/app/src/main/res/layout-land/call_numpad_bottom_sheet.xml b/app/src/main/res/layout-land/call_numpad_bottom_sheet.xml index 75dc310e96..ca38d7b800 100644 --- a/app/src/main/res/layout-land/call_numpad_bottom_sheet.xml +++ b/app/src/main/res/layout-land/call_numpad_bottom_sheet.xml @@ -22,6 +22,7 @@ android:background="@drawable/shape_call_bottom_sheet_background" android:clickable="true" android:focusable="true" + android:layoutDirection="ltr" android:visibility="@{viewModel.fullScreenMode || viewModel.pipMode ? View.INVISIBLE : View.VISIBLE}" app:behavior_hideable="true" app:behavior_peekHeight="0dp" diff --git a/app/src/main/res/layout/call_numpad_bottom_sheet.xml b/app/src/main/res/layout/call_numpad_bottom_sheet.xml index 262c303185..d1cd6cc7e7 100644 --- a/app/src/main/res/layout/call_numpad_bottom_sheet.xml +++ b/app/src/main/res/layout/call_numpad_bottom_sheet.xml @@ -22,6 +22,7 @@ android:background="@drawable/shape_call_bottom_sheet_background" android:clickable="true" android:focusable="true" + android:layoutDirection="ltr" android:visibility="@{viewModel.fullScreenMode || viewModel.pipMode ? View.INVISIBLE : View.VISIBLE}" app:behavior_hideable="true" app:behavior_peekHeight="0dp" diff --git a/app/src/main/res/layout/start_call_numpad_bottom_sheet.xml b/app/src/main/res/layout/start_call_numpad_bottom_sheet.xml index a7991dfdb2..11543cca56 100644 --- a/app/src/main/res/layout/start_call_numpad_bottom_sheet.xml +++ b/app/src/main/res/layout/start_call_numpad_bottom_sheet.xml @@ -22,6 +22,7 @@ android:background="@drawable/shape_bottom_sheet_background" android:clickable="true" android:focusable="true" + android:layoutDirection="ltr" app:behavior_hideable="true" app:behavior_peekHeight="0dp" app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"> From ac1931c22a644f5aaaa4831d5753693708d7cc2b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 10 Mar 2026 19:48:29 +0000 Subject: [PATCH 486/593] Updated translations from Weblate --- app/src/main/res/values-cs/strings.xml | 6 ++++++ app/src/main/res/values-de/strings.xml | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 91cb60b1e4..f65717cb34 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -915,4 +915,10 @@ Vymazat autentizační údaje, které již nejsou spojeny s žádným účtem Výběr vyzváněcích tónů není k dispozici! Koncově šifrováno + <redacted> + Nezobrazovat obsah zprávy v oznámení systému Android + Zobrazit minulé schůzky + Ověření telefonního čísla není k dispozici + Ověření telefonního čísla není k dispozici, použijte prosím proces vytvoření e-mailového účtu. + Registrace s e-mailem diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index ed6356ae1c..5dc7824249 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -906,4 +906,8 @@ Klicken Sie auf diese Benachrichtigung, um das Problem zu beheben Ihr Gesprächspartner hört Sie nicht! + <zensiert> + End-to-End-verschlüsselt + Nachrichteninhalt nicht in Android-Benachrichtigungen anzeigen + Vergangene Meetings anzeigen From 044fcab63165f95ec8f0d4640f9bf72176c729ee Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 11 Mar 2026 09:36:47 +0100 Subject: [PATCH 487/593] Bumped version code for next beta --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c0e98a2c77..96cfeea184 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -106,7 +106,7 @@ android { applicationId = packageName minSdk = 28 targetSdk = 36 - versionCode = 601006 // 6.01.006 + versionCode = 601007 // 6.01.007 versionName = "6.2.0-beta" manifestPlaceholders["appAuthRedirectScheme"] = packageName From fa75b1547d2039ec653f5a1454a6448f6327c2a1 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 11 Mar 2026 10:14:19 +0100 Subject: [PATCH 488/593] Updated firebase & coil dependencies --- gradle/libs.versions.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7a380384c0..43cd9458c0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ agp = "9.1.0" kotlin = "2.3.10" gmsGoogleServices = "4.4.4" firebaseCrashlytics = "3.0.6" -firebaseBomVersion = "34.8.0" +firebaseBomVersion = "34.10.0" #noinspection NewerVersionAvailable ktlint = "12.3.0" @@ -27,7 +27,7 @@ flexbox = "3.0.0" material = "1.13.0" #noinspection NewerVersionAvailable protobuf = "3.25.5" -coil = "3.3.0" +coil = "3.4.0" dotsIndicator = "5.1.0" photoview = "2.3.0" openidAppauth = "0.11.1" From d47a77520b23383fe4a26c9495344bdfd3861189 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 12 Mar 2026 18:19:50 +0100 Subject: [PATCH 489/593] Switched to gradle toolchain --- gradle/gradle-daemon-jvm.properties | 13 +++++++++++++ settings.gradle.kts | 3 +++ 2 files changed, 16 insertions(+) create mode 100644 gradle/gradle-daemon-jvm.properties diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties new file mode 100644 index 0000000000..42bccabdc0 --- /dev/null +++ b/gradle/gradle-daemon-jvm.properties @@ -0,0 +1,13 @@ +#This file is generated by updateDaemonJvm +toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect +toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect +toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect +toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect +toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/e99bae143b75f9a10ead10248f02055e/redirect +toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/04e088f8677de3b384108493cc9481d0/redirect +toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/56a19bc915b9ba2eb62ba7554c61b919/redirect +toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/398ffe3949748bfb1d5636f023d228fd/redirect +toolchainUrl.WINDOWS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/2ddfb13e430f2b3a94c9c937d8d2f67e/redirect +toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/932015f6361ccaead0c6d9b8717ed96e/redirect +toolchainVendor=JETBRAINS +toolchainVersion=21 diff --git a/settings.gradle.kts b/settings.gradle.kts index c0d0a1f84f..e2c7042407 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -5,6 +5,9 @@ pluginManagement { gradlePluginPortal() } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) From 0a63736451b8162a9c68850190172d111c7ba82e Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 13 Mar 2026 16:37:57 +0100 Subject: [PATCH 490/593] Fixed media grid using 2 lines issue when sending exactly 3 files with text --- .../org/linphone/ui/main/chat/model/MessageModel.kt | 4 +++- .../main/res/layout/chat_bubble_content_grid_cell.xml | 10 ++++++---- app/src/main/res/layout/chat_bubble_incoming.xml | 3 +-- app/src/main/res/layout/chat_bubble_outgoing.xml | 3 +-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt index 97bcd1bf52..d1e7520405 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt @@ -434,7 +434,9 @@ class MessageModel val contents = chatMessage.contents allFilesDownloaded = true - val exactly4Contents = contents.size == 4 + val exactly4Contents = contents.count { + it.isFile || it.isFileTransfer + } == 4 for (content in contents) { val isFileEncrypted = content.isFileEncrypted diff --git a/app/src/main/res/layout/chat_bubble_content_grid_cell.xml b/app/src/main/res/layout/chat_bubble_content_grid_cell.xml index 4378bfc62b..33d4ba3d4b 100644 --- a/app/src/main/res/layout/chat_bubble_content_grid_cell.xml +++ b/app/src/main/res/layout/chat_bubble_content_grid_cell.xml @@ -18,23 +18,25 @@ android:layout_height="wrap_content" app:layout_wrapBefore="@{model.flexboxLayoutWrapBefore}"> - - - diff --git a/app/src/main/res/layout/chat_bubble_outgoing.xml b/app/src/main/res/layout/chat_bubble_outgoing.xml index ed494e3aff..237a06551d 100644 --- a/app/src/main/res/layout/chat_bubble_outgoing.xml +++ b/app/src/main/res/layout/chat_bubble_outgoing.xml @@ -5,7 +5,6 @@ - From ff7783c48a0493f54a92c0046b92c98ff1b6fc6c Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 13 Mar 2026 17:41:51 +0100 Subject: [PATCH 491/593] Replaced delete message dialog by bottom sheet menu --- .../chat/fragment/ConversationFragment.kt | 45 ++------ .../fragment/ConversationsListFragment.kt | 3 +- .../chat/fragment/MessageDialogFragment.kt | 83 +++++++++++++ .../java/org/linphone/utils/DialogUtils.kt | 18 --- app/src/main/res/color/danger_500.xml | 1 + .../res/layout/chat_message_delete_menu.xml | 54 +++++++++ .../res/layout/dialog_delete_chat_message.xml | 109 ------------------ app/src/main/res/values-cs/strings.xml | 3 - app/src/main/res/values-de/strings.xml | 3 - app/src/main/res/values-fr/strings.xml | 5 +- app/src/main/res/values-night/themes.xml | 2 + app/src/main/res/values/attrs.xml | 1 + app/src/main/res/values/strings.xml | 5 +- app/src/main/res/values/styles.xml | 4 +- app/src/main/res/values/themes.xml | 2 + 15 files changed, 162 insertions(+), 176 deletions(-) create mode 100644 app/src/main/java/org/linphone/ui/main/chat/fragment/MessageDialogFragment.kt create mode 100644 app/src/main/res/layout/chat_message_delete_menu.xml delete mode 100644 app/src/main/res/layout/dialog_delete_chat_message.xml diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 544b46ade1..0c6f0ab9fc 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -95,7 +95,6 @@ import org.linphone.utils.setKeyboardInsetListener import org.linphone.utils.showKeyboard import androidx.core.net.toUri import org.linphone.ui.main.chat.adapter.ConversationParticipantsAdapter -import org.linphone.ui.main.chat.model.MessageDeleteDialogModel import org.linphone.utils.ShortcutUtils import kotlin.collections.arrayListOf @@ -940,7 +939,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { if (model != null) { if (model.isOutgoing && !(model.hasBeenRetracted.value ?: false)) { // For sent messages let user choose between delete locally / delete for everyone - showHowToDeleteMessageDialog(model) + showHowToDeleteMessageMenu(model) } else { // For received messages or retracted sent ones you can only delete locally viewModel.deleteChatMessage(model) @@ -1657,43 +1656,23 @@ open class ConversationFragment : SlidingPaneChildFragment() { } } - private fun showHowToDeleteMessageDialog(model: MessageModel) { + private fun showHowToDeleteMessageMenu(model: MessageModel) { val canBeRetracted = messageLongPressViewModel.canBeRemotelyDeleted.value == true - val dialogModel = MessageDeleteDialogModel(canBeRetracted) + val modalBottomSheet = MessageDialogFragment( + canBeRetracted, + { // onDismiss - val dialog = DialogUtils.getHowToDeleteMessageDialog( - requireActivity(), - dialogModel - ) - - dialogModel.dismissEvent.observe(viewLifecycleOwner) { - it.consume { - dialog.dismiss() - } - } - - dialogModel.cancelEvent.observe(viewLifecycleOwner) { - it.consume { - dialog.dismiss() - } - } - - dialogModel.deleteLocallyEvent.observe(viewLifecycleOwner) { - it.consume { + }, + { // onMarkConversationAsRead Log.i("$TAG Deleting chat message locally") viewModel.deleteChatMessage(model) - dialog.dismiss() - } - } - - dialogModel.deleteForEveryoneEvent.observe(viewLifecycleOwner) { - it.consume { + }, + { // onToggleMute Log.i("$TAG Deleting chat message (content) for everyone") viewModel.deleteChatMessageForEveryone(model) - dialog.dismiss() } - } - - dialog.show() + ) + modalBottomSheet.show(parentFragmentManager, MessageDialogFragment.TAG) + bottomSheetDialog = modalBottomSheet } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt index c9279d3a66..d5db5cd232 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt @@ -47,7 +47,6 @@ import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel import org.linphone.ui.main.fragment.AbstractMainFragment -import org.linphone.ui.main.history.fragment.HistoryMenuDialogFragment import org.linphone.utils.DialogUtils import org.linphone.utils.Event import org.linphone.utils.LinphoneUtils @@ -177,7 +176,7 @@ class ConversationsListFragment : AbstractMainFragment() { model.leaveGroup() } ) - modalBottomSheet.show(parentFragmentManager, HistoryMenuDialogFragment.TAG) + modalBottomSheet.show(parentFragmentManager, ConversationDialogFragment.TAG) bottomSheetDialog = modalBottomSheet } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/MessageDialogFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/MessageDialogFragment.kt new file mode 100644 index 0000000000..125ca4c7e9 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/MessageDialogFragment.kt @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2010-2026 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.ui.main.chat.fragment + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.UiThread +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.linphone.databinding.ChatMessageDeleteMenuBinding + +@UiThread +class MessageDialogFragment( + private val canDeleteForEveryone: Boolean, + private val onDismiss: (() -> Unit)? = null, + private val onDeleteLocally: (() -> Unit)? = null, + private val onDeleteForEveryone: (() -> Unit)? = null +) : BottomSheetDialogFragment() { + companion object { + const val TAG = "MessageDialogFragment" + } + + override fun onCancel(dialog: DialogInterface) { + onDismiss?.invoke() + super.onCancel(dialog) + } + + override fun onDismiss(dialog: DialogInterface) { + onDismiss?.invoke() + super.onDismiss(dialog) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog + // Makes sure all menu entries are visible, + // required for landscape mode (otherwise only first item is visible) + dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED + return dialog + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val view = ChatMessageDeleteMenuBinding.inflate(layoutInflater) + view.canDeleteForEveryone = canDeleteForEveryone + + view.setDeleteForMeClickListener { + onDeleteLocally?.invoke() + dismiss() + } + + view.setDeleteForEveryoneClickListener { + onDeleteForEveryone?.invoke() + dismiss() + } + + return view.root + } +} diff --git a/app/src/main/java/org/linphone/utils/DialogUtils.kt b/app/src/main/java/org/linphone/utils/DialogUtils.kt index 5c4fa287bc..5e27ee26c5 100644 --- a/app/src/main/java/org/linphone/utils/DialogUtils.kt +++ b/app/src/main/java/org/linphone/utils/DialogUtils.kt @@ -67,9 +67,7 @@ import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel import org.linphone.ui.main.model.GroupSetOrEditSubjectDialogModel import androidx.core.graphics.drawable.toDrawable import org.linphone.databinding.DialogAssistantCreateAccountPhoneNumberValidationNotAvailableBinding -import org.linphone.databinding.DialogDeleteChatMessageBinding import org.linphone.databinding.DialogManageAccountOutboundProxyHelpBinding -import org.linphone.ui.main.chat.model.MessageDeleteDialogModel class DialogUtils { companion object { @@ -550,22 +548,6 @@ class DialogUtils { return getDialog(context, binding) } - @UiThread - fun getHowToDeleteMessageDialog( - context: Context, - viewModel: MessageDeleteDialogModel - ): Dialog { - val binding: DialogDeleteChatMessageBinding = DataBindingUtil.inflate( - LayoutInflater.from(context), - R.layout.dialog_delete_chat_message, - null, - false - ) - binding.viewModel = viewModel - - return getDialog(context, binding) - } - @UiThread private fun getDialog(context: Context, binding: ViewDataBinding): Dialog { val dialog = Dialog(context, R.style.Theme_LinphoneDialog) diff --git a/app/src/main/res/color/danger_500.xml b/app/src/main/res/color/danger_500.xml index ea14a77612..3a40735a8b 100644 --- a/app/src/main/res/color/danger_500.xml +++ b/app/src/main/res/color/danger_500.xml @@ -1,5 +1,6 @@ + diff --git a/app/src/main/res/layout/chat_message_delete_menu.xml b/app/src/main/res/layout/chat_message_delete_menu.xml new file mode 100644 index 0000000000..bdd4f27144 --- /dev/null +++ b/app/src/main/res/layout/chat_message_delete_menu.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_delete_chat_message.xml b/app/src/main/res/layout/dialog_delete_chat_message.xml deleted file mode 100644 index e5f25746f8..0000000000 --- a/app/src/main/res/layout/dialog_delete_chat_message.xml +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index f65717cb34..4413876496 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -889,9 +889,6 @@ Tento PDF soubor nelze otevřít, může být poškozen Zpráva se upravuje Upraveno - Smazat tuto zprávu? - Pro mě - Pro všechny Tato zpráva bude smazána Tuto zprávu jste smazali Nenalezeni žádní účastníci diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5dc7824249..1fe4f3eb4b 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -883,9 +883,6 @@ PDF-Datei kann nicht geöffnet werden, möglicherweise ist sie beschädigt Nachricht wird bearbeitet Bearbeitet - Nachricht löschen? - Für mich - Für alle Diese Nachricht wurde gelöscht Sie haben diese Nachricht gelöscht Keine Teilnehmer gefunden diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b2d20ec06b..6512a5d771 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -578,9 +578,8 @@ Impossible d\'ouvrir ce PDF, le fichier est peut-être corrompu Modification du message Modifié - Supprimer le message ? - Pour moi - Pour tout le monde + Supprimer pour moi + Supprimer pour tout le monde Le message a été supprimé Vous avez supprimé le message Participants diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 862223598e..02ca799858 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -82,6 +82,8 @@ @color/gray_main2_400 @color/background_color_alt_dark_mode + + @color/gray_500 - - - - - - + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 1d6352027c..e4c801ef67 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -12,10 +12,7 @@ - - - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 98036be3c3..c2ae7003bb 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -11,10 +11,7 @@ #303030 #FFEACB - #80FFEACB - #FFB266 #FF5E00 - #B72D00 #EEF6F8 #DFECF2 @@ -60,39 +57,31 @@ #80FFFFFF - #FFF5D6 - #80FFF5D6 - #FFE799 - #F5BC00 - #A37D00 - - #DCF9E7 - #80DCF9E7 - #A8F0C2 - #25D366 - #1C9C4B - - #D6F4FF - #80D6F4FF - #99E4FF - #00aff0 - #0078A3 - - #FBE1DA - #80FBE1DA - #F5B53A - #E14318 - #A63211 - - #FFD6F1 - #80FFD6F1 - #FF99DD - #FF00A9 - #B8007A - - #FFD6FF - #80FFD6FF - #FF99FF - #800080 - #520052 + #F2D2C7 + #C86B45 + + #E9E7F2 + #7F79B5 + + #FBFCE9 + #D9A441 + + #F6BBC3 + #7A1E39 + + #DEF2E8 + #7AC9A1 + + #FFD3D6 + #F26B5E + + #DBBED9 + #9B4F96 + + #D9D9D9 + #8A939B + + #C5E0F3 + #669ED7 + \ No newline at end of file diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 001d3e61bc..dc0eecb016 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -43,6 +43,9 @@ 90dp 11dp + 20dp + 6dp + 12dp 20dp 20dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7348a64814..df201b6135 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -901,6 +901,15 @@ Red Pink Purple + Terracotta + Lavender + Honey + Burgundy + Mint + Coral + Plum + Titanium + Mineral Blue No result found… diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 90fa697d84..9bcabea19c 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -88,6 +88,8 @@ @drawable/primary_button_background @dimen/primary_secondary_buttons_label_padding @dimen/primary_secondary_buttons_label_padding + @dimen/primary_secondary_buttons_label_start_end_padding + @dimen/primary_secondary_buttons_label_start_end_padding - - - - - - + + + + + + @@ -146,27 +143,39 @@ @color/gray_600 - + + + + + + - - - - - From d8536c8799245f65f7b2c950ecfe112c8161d941 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 23 Mar 2026 09:18:37 +0000 Subject: [PATCH 498/593] Updated translations from Weblate --- app/src/main/res/values-ca/strings.xml | 8 ++++++++ app/src/main/res/values-cs/strings.xml | 11 +++++++++++ app/src/main/res/values-de/strings.xml | 14 ++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 8fd6d6c643..068afc9f79 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -11,4 +11,12 @@ Assegurada No t\'escolten! Un aplicatiu de comunicació segur, de codi obert i en francès. + Esborra-ho per a mi + Has esborrat aquest missatge + Participants + No s\'ha trobat cap participant + Afegeix participants + Aquest missatge ha estat esborrat + Grup de membres (%s) + Esborra-ho per a tothom diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 4413876496..bf604ebf36 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -918,4 +918,15 @@ Ověření telefonního čísla není k dispozici Ověření telefonního čísla není k dispozici, použijte prosím proces vytvoření e-mailového účtu. Registrace s e-mailem + Terakota + Levandule + Med + Burgundsko + Máta + Korál + Švestka + Titan + Minerální modrá + Smazat to pro mě + Smazat to pro všechny diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1fe4f3eb4b..8bc8fc4c27 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -907,4 +907,18 @@ End-to-End-verschlüsselt Nachrichteninhalt nicht in Android-Benachrichtigungen anzeigen Vergangene Meetings anzeigen + Die Überprüfung der Telefonnummer ist nicht verfügbar. Bitte nutzen Sie den Prozess zur Erstellung eines E-Mail-Kontos. + Die Überprüfung der Telefonnummer ist nicht verfügbar + Mit E-Mail registrieren + Für alle löschen + Terrakotta + Lavendel + Honig + Burgund + Minze + Koral + Pflaume + Titan + Mineralblau + Für mich löschen From c5d83b5db38ebce878e61bfd816c1f4d1b527a36 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 23 Mar 2026 10:19:29 +0100 Subject: [PATCH 499/593] Bumped version code for next public beta --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 96cfeea184..ab97fe4367 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -106,7 +106,7 @@ android { applicationId = packageName minSdk = 28 targetSdk = 36 - versionCode = 601007 // 6.01.007 + versionCode = 601008 // 6.01.008 versionName = "6.2.0-beta" manifestPlaceholders["appAuthRedirectScheme"] = packageName From facc19f60ecf0d06355dd56ad6e5d0befb640d2b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 26 Mar 2026 15:37:50 +0100 Subject: [PATCH 500/593] Minor code improvements, bumped gradle & firebase dependencies --- .../linphone/compatibility/Compatibility.kt | 6 ++-- .../java/org/linphone/ui/call/CallActivity.kt | 1 - .../ui/call/fragment/ConversationFragment.kt | 2 +- .../chat/model/MessageDeleteDialogModel.kt | 35 ------------------- .../layout-land/call_outgoing_fragment.xml | 1 + ..._conference_participants_list_fragment.xml | 1 + .../res/layout/call_outgoing_fragment.xml | 1 + .../call_video_local_preview_surface.xml | 1 + .../main/res/layout/calls_list_fragment.xml | 1 + app/src/main/res/values-cs/strings.xml | 7 ---- app/src/main/res/values-de/strings.xml | 7 ---- app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 7 ---- app/src/main/res/values-nl/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-sk/strings.xml | 1 - app/src/main/res/values-uk/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 1 - app/src/main/res/values/strings.xml | 7 ---- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 22 files changed, 11 insertions(+), 77 deletions(-) delete mode 100644 app/src/main/java/org/linphone/ui/main/chat/model/MessageDeleteDialogModel.kt diff --git a/app/src/main/java/org/linphone/compatibility/Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Compatibility.kt index 3ed91c9a4f..99dd1d02bb 100644 --- a/app/src/main/java/org/linphone/compatibility/Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Compatibility.kt @@ -53,15 +53,15 @@ class Compatibility { notification: Notification, foregroundServiceType: Int ): Boolean { - if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) { - return Api34Compatibility.startServiceForeground( + return if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) { + Api34Compatibility.startServiceForeground( service, id, notification, foregroundServiceType ) } else { - return Api28Compatibility.startServiceForeground(service, id, notification) + Api28Compatibility.startServiceForeground(service, id, notification) } } diff --git a/app/src/main/java/org/linphone/ui/call/CallActivity.kt b/app/src/main/java/org/linphone/ui/call/CallActivity.kt index befed1851b..bcbe1ba31b 100644 --- a/app/src/main/java/org/linphone/ui/call/CallActivity.kt +++ b/app/src/main/java/org/linphone/ui/call/CallActivity.kt @@ -43,7 +43,6 @@ import androidx.databinding.DataBindingUtil import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController -import androidx.navigation.fragment.findNavController import androidx.window.layout.FoldingFeature import androidx.window.layout.WindowInfoTracker import androidx.window.layout.WindowLayoutInfo diff --git a/app/src/main/java/org/linphone/ui/call/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/ConversationFragment.kt index c5c5660cd0..4171344d18 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/ConversationFragment.kt @@ -120,7 +120,7 @@ class ConversationFragment : ConversationFragment() { val layout = layoutInflater.inflate(R.layout.call_video_local_preview_surface, binding.constraintLayout, false) binding.constraintLayout.addView(layout) - localPreviewVideoSurface = layout.findViewById(R.id.local_preview_video_surface) + localPreviewVideoSurface = layout.findViewById(R.id.local_preview_video_surface) callViewModel.isSendingVideo.observe(viewLifecycleOwner) { sending -> coreContext.postOnCoreThread { core -> diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageDeleteDialogModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageDeleteDialogModel.kt deleted file mode 100644 index c4bf5b4514..0000000000 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageDeleteDialogModel.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.linphone.ui.main.chat.model - -import androidx.annotation.UiThread -import androidx.lifecycle.MutableLiveData -import org.linphone.utils.Event - -class MessageDeleteDialogModel(val canBeRetracted: Boolean) { - val dismissEvent = MutableLiveData>() - - val cancelEvent = MutableLiveData>() - - val deleteLocallyEvent = MutableLiveData>() - - val deleteForEveryoneEvent = MutableLiveData>() - - @UiThread - fun dismiss() { - dismissEvent.value = Event(true) - } - - @UiThread - fun cancel() { - cancelEvent.value = Event(true) - } - - @UiThread - fun deleteLocally() { - deleteLocallyEvent.value = Event(true) - } - - @UiThread - fun deleteForEveryone() { - deleteForEveryoneEvent.value = Event(true) - } -} diff --git a/app/src/main/res/layout-land/call_outgoing_fragment.xml b/app/src/main/res/layout-land/call_outgoing_fragment.xml index 2cf0ca3825..8db5468a42 100644 --- a/app/src/main/res/layout-land/call_outgoing_fragment.xml +++ b/app/src/main/res/layout-land/call_outgoing_fragment.xml @@ -117,6 +117,7 @@ app:layout_constraintTop_toTopOf="@id/name_address" app:layout_constraintBottom_toBottomOf="@id/name_address" /> + + + + + %s ms selhalo Adaptivní datový tok - Povolit video Povolit FEC pro video Vibrovat při příchozím hovoru Automaticky spouštět nahrávání hovorů @@ -722,12 +721,6 @@ Stáhnout Sdílet Oranžová - Žlutá - Zelená - Modrá - Červená - Růžová - Fialová Zde se zobrazí vybraní účastnící Chyba při připojení účtu(ů) Zvolený účet je momentálně zákázán diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8bc8fc4c27..5c458ce1a9 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -170,7 +170,6 @@ %s ms fehlgeschlagen Adaptive Geschwindigkeitsregelung - Video aktivieren Video FEC aktivieren Vibrieren während ein eingehender Anruf klingelt Automatische Anrufaufzeichnung starten @@ -661,12 +660,6 @@ Herunterladen Teilen Orange - Gelb - Grün - Blau - Rot - Rosa - Lila Kein Ergebnis gefunden… Ausgewählte Teilnehmer erscheinen hier Verbindungsfehler bei den Konten diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 66e17cf964..1269e1e7a9 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -187,7 +187,6 @@ %s ms fallido Control de velocidad adaptativo - Habilitar video Habilitar FEC de vídeo Vibrar mientras suena la llamada entrante Iniciar automáticamente la grabación de llamadas diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 52cc1056ae..0a495859cf 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -220,7 +220,6 @@ %s ms échec Contrôle automatique de la qualité - Autoriser la vidéo Activer la FEC vidéo Vibration lors de l\'appel Enregistrement automatique des appels @@ -852,12 +851,6 @@ Orange - Jaune - Vert - Bleu - Rouge - Rose - Violet Terracotta Lavande Miel diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 2967ea2353..7cdcebc524 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -140,7 +140,6 @@ Geen echo Mislukt Adaptieve bitrate-regeling - Video inschakelen Video-FEC inschakelen Trillen tijdens inkomend gesprek Oproepen automatisch opnemen diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 95dcb2146d..3b66ea56c5 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -222,7 +222,6 @@ sem eco %s ms falhou - Ativar vídeo Ativar FEC de vídeo Iniciar gravação de chamadas automaticamente Alterar toque diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 9a210b0c04..545e24af00 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -292,7 +292,6 @@ %s мс калибровка не удалась Адаптивный контроль скорости - Включить видео Включить видео FEC Автоматически начинать запись звонков Изменить рингтон diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index f2c0a0f1ae..d962a68279 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -176,7 +176,6 @@ bez ozveny %s ms zlyhalo - Povoliť video Povoliť FEC pre video Automaticky spustiť nahrávanie hovorov Zmeniť vyzváňací tón diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index d06575b008..cecedbd2c2 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -277,7 +277,6 @@ %s мс Калібрування не вдалося Адаптивне керування швидкістю - Увімкнути відео Увімкнути FEC відео Вібрація під час вхідного дзвінка Автоматично починати запис дзвінків diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index d8b34f69f4..8d73c19de5 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -199,7 +199,6 @@ %s 毫秒 失败 自适应速率控制 - 启用视频 启用视频前向纠错FEC 来电铃响时振动 自动开始录制通话 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index df201b6135..bd5cedc2c5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -262,7 +262,6 @@ %s ms failed Adaptive rate control - Enable video Enable video FEC Vibrate while incoming call is ringing Automatically start recording calls @@ -895,12 +894,6 @@ Orange - Yellow - Green - Blue - Red - Pink - Purple Terracotta Lavender Honey diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a68dd6dcf3..6d6765c907 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ agp = "9.1.0" kotlin = "2.3.20" gmsGoogleServices = "4.4.4" firebaseCrashlytics = "3.0.6" -firebaseBomVersion = "34.10.0" +firebaseBomVersion = "34.11.0" #noinspection NewerVersionAvailable ktlint = "12.3.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 98556d5d93..80618190c8 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Jun 22 12:11:25 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 84ad32a65e545cb2688667f14965adb78bea26da Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 31 Mar 2026 14:27:03 +0200 Subject: [PATCH 501/593] Fixed sharing text multiple times from a third party app to Linphone not working after first one --- .../java/org/linphone/ui/main/MainActivity.kt | 23 +++++++++++-------- .../chat/fragment/ConversationFragment.kt | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/MainActivity.kt b/app/src/main/java/org/linphone/ui/main/MainActivity.kt index f50b4cd9df..f10149c4ff 100644 --- a/app/src/main/java/org/linphone/ui/main/MainActivity.kt +++ b/app/src/main/java/org/linphone/ui/main/MainActivity.kt @@ -642,15 +642,6 @@ class MainActivity : GenericActivity() { private fun handleSendIntent(intent: Intent, multiple: Boolean) { val parcelablesUri = arrayListOf() - - if (intent.type == "text/plain") { - Log.i("$TAG Intent type is [${intent.type}], expecting text in Intent.EXTRA_TEXT") - intent.getStringExtra(Intent.EXTRA_TEXT)?.let { extraText -> - Log.i("$TAG Found extra text in intent, long of [${extraText.length}]") - sharedViewModel.textToShareFromIntent.value = extraText - } - } - if (multiple) { val parcelables = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM) @@ -698,11 +689,23 @@ class MainActivity : GenericActivity() { if (path != null) list.add(path) } + var textToShare = "" + if (intent.type == "text/plain") { + Log.i("$TAG Intent type is [${intent.type}], expecting text in Intent.EXTRA_TEXT") + textToShare = intent.getStringExtra(Intent.EXTRA_TEXT).orEmpty() + if (textToShare.isEmpty()) { + Log.e("$TAG Intent.EXTRA_TEXT not found in intent!") + } else { + Log.i("$TAG Found extra text in intent, long of [${textToShare.length}]") + } + } + if (list.isNotEmpty()) { sharedViewModel.filesToShareFromIntent.value = list } else { - if (sharedViewModel.textToShareFromIntent.value.orEmpty().isNotEmpty()) { + if (textToShare.isNotEmpty()) { Log.i("$TAG Found plain text to share") + sharedViewModel.textToShareFromIntent.value = textToShare } else { Log.w("$TAG Failed to find at least one file or text to share!") } diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 3a2b156d22..02935e759a 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -1002,7 +1002,7 @@ open class ConversationFragment : SlidingPaneChildFragment() { } sharedViewModel.textToShareFromIntent.observe(viewLifecycleOwner) { text -> - if (text.isNotEmpty()) { + if (text.isNotEmpty() && sharedViewModel.displayedChatRoom != null) { Log.i("$TAG Found text to share from intent") sendMessageViewModel.textToSend.value = text From 24711582c6b59c0d48022fd3b0744293d4d3ba2f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 3 Apr 2026 09:06:18 +0200 Subject: [PATCH 502/593] Remove duplicated entry in linphonerc_default --- app/src/main/assets/linphonerc_default | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/assets/linphonerc_default b/app/src/main/assets/linphonerc_default index 0b7f694fd3..389663a9e7 100644 --- a/app/src/main/assets/linphonerc_default +++ b/app/src/main/assets/linphonerc_default @@ -3,7 +3,6 @@ [sip] contact="Linphone Android" -use_info=0 use_ipv6=1 keepalive_period=30000 sip_port=-1 From 5ac5cec8e86b7f148e4ef04c11b96f77e18acfd2 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 3 Apr 2026 14:17:33 +0200 Subject: [PATCH 503/593] Hide participants list header as well in read-only group chat room --- app/src/main/res/layout/chat_info_fragment.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/chat_info_fragment.xml b/app/src/main/res/layout/chat_info_fragment.xml index 5dd2ec24f8..a5a2315f74 100644 --- a/app/src/main/res/layout/chat_info_fragment.xml +++ b/app/src/main/res/layout/chat_info_fragment.xml @@ -352,7 +352,7 @@ android:onClick="@{() -> viewModel.toggleParticipantsExpand()}" android:padding="10dp" android:text="@{viewModel.participantsLabel, default=@string/conversation_info_participants_list_title}" - android:visibility="@{viewModel.isGroup ? View.VISIBLE : View.GONE}" + android:visibility="@{viewModel.isGroup && !viewModel.isReadOnly ? View.VISIBLE : View.GONE}" android:background="@drawable/squircle_transparent_button_background" app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintEnd_toEndOf="parent" From 07b33c3e3a7cf4c5a3d20ad1c255e115e60b7289 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 3 Apr 2026 14:22:38 +0200 Subject: [PATCH 504/593] Hide do ZRTP SAS validation again while in conference --- .../layout-land/call_media_encryption_stats_bottom_sheet.xml | 2 +- .../res/layout/call_media_encryption_stats_bottom_sheet.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout-land/call_media_encryption_stats_bottom_sheet.xml b/app/src/main/res/layout-land/call_media_encryption_stats_bottom_sheet.xml index ed9cfa4033..6b43d9184c 100644 --- a/app/src/main/res/layout-land/call_media_encryption_stats_bottom_sheet.xml +++ b/app/src/main/res/layout-land/call_media_encryption_stats_bottom_sheet.xml @@ -142,7 +142,7 @@ android:layout_height="wrap_content" android:layout_margin="15dp" android:text="@string/call_do_zrtp_sas_validation_again" - android:visibility="@{model.isMediaEncryptionZrtp ? View.VISIBLE : View.GONE}" + android:visibility="@{model.isMediaEncryptionZrtp && !viewModel.conferenceModel.isCurrentCallInConference() ? View.VISIBLE : View.GONE}" app:layout_columnSpan="2" app:layout_gravity="center_horizontal"/> diff --git a/app/src/main/res/layout/call_media_encryption_stats_bottom_sheet.xml b/app/src/main/res/layout/call_media_encryption_stats_bottom_sheet.xml index e25eb89c1c..cb9cfea1d0 100644 --- a/app/src/main/res/layout/call_media_encryption_stats_bottom_sheet.xml +++ b/app/src/main/res/layout/call_media_encryption_stats_bottom_sheet.xml @@ -136,7 +136,7 @@ android:layout_margin="15dp" android:text="@string/call_do_zrtp_sas_validation_again" android:layout_gravity="center_horizontal" - android:visibility="@{model.isMediaEncryptionZrtp ? View.VISIBLE : View.GONE}"/> + android:visibility="@{model.isMediaEncryptionZrtp && !viewModel.conferenceModel.isCurrentCallInConference() ? View.VISIBLE : View.GONE}"/> From a3742824429ac4938540fd70fbee1fda68a3f22e Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 3 Apr 2026 14:31:28 +0200 Subject: [PATCH 505/593] Allow multi lines in sceduled meeting description --- app/src/main/res/layout/meeting_edit_fragment.xml | 2 +- app/src/main/res/layout/meeting_schedule_fragment.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/meeting_edit_fragment.xml b/app/src/main/res/layout/meeting_edit_fragment.xml index 3b4044fec5..5a8115d2de 100644 --- a/app/src/main/res/layout/meeting_edit_fragment.xml +++ b/app/src/main/res/layout/meeting_edit_fragment.xml @@ -259,7 +259,7 @@ android:text="@={viewModel.description}" android:textSize="14sp" android:textColor="?attr/color_main2_600" - android:inputType="text|textCapSentences" + android:inputType="text|textCapSentences|textMultiLine" android:drawableStart="@drawable/file_text" android:drawablePadding="8dp" android:drawableTint="?attr/color_main2_600" diff --git a/app/src/main/res/layout/meeting_schedule_fragment.xml b/app/src/main/res/layout/meeting_schedule_fragment.xml index 85c6e05a36..25c8a781dd 100644 --- a/app/src/main/res/layout/meeting_schedule_fragment.xml +++ b/app/src/main/res/layout/meeting_schedule_fragment.xml @@ -397,7 +397,7 @@ android:text="@={viewModel.description}" android:textSize="14sp" android:textColor="?attr/color_main2_600" - android:inputType="text|textCapSentences" + android:inputType="text|textCapSentences|textMultiLine" android:drawableStart="@drawable/file_text" android:drawablePadding="8dp" android:drawableTint="?attr/color_main2_600" From e2db7fe1149100e24584c3ecb725d860afaf8f76 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 3 Apr 2026 14:39:15 +0200 Subject: [PATCH 506/593] Fixed empty list when filtering meetings if there is no result --- .../layout-land/meetings_list_fragment.xml | 33 +++++++++++++++++++ .../res/layout/meetings_list_fragment.xml | 33 +++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/app/src/main/res/layout-land/meetings_list_fragment.xml b/app/src/main/res/layout-land/meetings_list_fragment.xml index 61f7f0e054..5b8d7098be 100644 --- a/app/src/main/res/layout-land/meetings_list_fragment.xml +++ b/app/src/main/res/layout-land/meetings_list_fragment.xml @@ -32,6 +32,12 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + + + + + + + + + + Date: Fri, 3 Apr 2026 14:46:04 +0200 Subject: [PATCH 507/593] Show call transfer icon in calls list shown during call transfer instead of call state --- .../java/org/linphone/ui/call/adapter/CallsListAdapter.kt | 4 +++- .../org/linphone/ui/call/fragment/TransferCallFragment.kt | 2 +- app/src/main/res/layout/call_list_cell.xml | 6 +++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/call/adapter/CallsListAdapter.kt b/app/src/main/java/org/linphone/ui/call/adapter/CallsListAdapter.kt index bd0d08654b..e87ede7b4a 100644 --- a/app/src/main/java/org/linphone/ui/call/adapter/CallsListAdapter.kt +++ b/app/src/main/java/org/linphone/ui/call/adapter/CallsListAdapter.kt @@ -28,12 +28,13 @@ import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import org.linphone.BR.showTransferIcon import org.linphone.R import org.linphone.databinding.CallListCellBinding import org.linphone.ui.call.model.CallModel import org.linphone.utils.Event -class CallsListAdapter : +class CallsListAdapter(private val showTransferIconInsteadOfCallState: Boolean = false) : ListAdapter(CallDiffCallback()) { var selectedAdapterPosition = -1 @@ -55,6 +56,7 @@ class CallsListAdapter : val viewHolder = ViewHolder(binding) binding.apply { lifecycleOwner = parent.findViewTreeLifecycleOwner() + showTransferIcon = showTransferIconInsteadOfCallState setOnClickListener { callClickedEvent.value = Event(model!!) diff --git a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt index 5fc4193fa3..087a55b693 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt @@ -87,7 +87,7 @@ class TransferCallFragment : GenericCallFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - callsAdapter = CallsListAdapter() + callsAdapter = CallsListAdapter(showTransferIconInsteadOfCallState = true) contactsAdapter = ConversationsContactsAndSuggestionsListAdapter() } diff --git a/app/src/main/res/layout/call_list_cell.xml b/app/src/main/res/layout/call_list_cell.xml index 986f32d476..c96518ee1c 100644 --- a/app/src/main/res/layout/call_list_cell.xml +++ b/app/src/main/res/layout/call_list_cell.xml @@ -14,6 +14,9 @@ + Date: Fri, 3 Apr 2026 14:51:52 +0200 Subject: [PATCH 508/593] Empty filter textfield when leaving new call/transfer call fragments --- .../main/java/org/linphone/ui/call/fragment/NewCallFragment.kt | 1 + .../java/org/linphone/ui/call/fragment/TransferCallFragment.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/call/fragment/NewCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/NewCallFragment.kt index 660fbee3fd..fd10c345e1 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/NewCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/NewCallFragment.kt @@ -228,6 +228,7 @@ class NewCallFragment : GenericCallFragment() { override fun onPause() { super.onPause() + viewModel.searchFilter.value = "" numberOrAddressPickerDialog?.dismiss() numberOrAddressPickerDialog = null } diff --git a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt index 087a55b693..a2c73f30d6 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt @@ -249,6 +249,7 @@ class TransferCallFragment : GenericCallFragment() { override fun onPause() { super.onPause() + viewModel.searchFilter.value = "" numberOrAddressPickerDialog?.dismiss() numberOrAddressPickerDialog = null } From 528ea325ad7fbc5fbcf795d05ba9cecc9308c91b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 3 Apr 2026 14:55:49 +0200 Subject: [PATCH 509/593] Change call recording button label while recording is ongoing --- app/src/main/res/layout/call_actions_bottom_sheet.xml | 2 +- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/call_actions_bottom_sheet.xml b/app/src/main/res/layout/call_actions_bottom_sheet.xml index 169de2ab82..882ec1bb79 100644 --- a/app/src/main/res/layout/call_actions_bottom_sheet.xml +++ b/app/src/main/res/layout/call_actions_bottom_sheet.xml @@ -285,7 +285,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:paddingBottom="15dp" - android:text="@string/call_action_record_call" + android:text="@{viewModel.isRecording ? @string/call_action_stop_record_call : @string/call_action_record_call, default=@string/call_action_record_call}" android:labelFor="@id/record_call" app:layout_constraintTop_toBottomOf="@id/record_call" app:layout_constraintStart_toStartOf="@id/calls_list_label" diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0a495859cf..5cb64b1720 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -712,6 +712,7 @@ Mettre en pause Reprendre Enregistrer + Arrêter l\'enregistrement Raccrocher Décliner Décrocher diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bd5cedc2c5..9d117bb4c0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -755,6 +755,7 @@ Pause Resume Record + Stop recording Hang up Decline Answer From e7c43d12589dfa117d3650bcefc7e2b858b4dff1 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 3 Apr 2026 15:00:41 +0200 Subject: [PATCH 510/593] Added a toast to let user know call recording as been stored when it's been toggled off --- .../java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt | 2 ++ app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 3 files changed, 4 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 9b5234a90e..4bb10d33a5 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -866,6 +866,8 @@ class CurrentCallViewModel isRecording.postValue(recording) if (recording) { showRecordingToast() + } else { + showGreenToast(R.string.call_has_been_recorded, R.drawable.record_fill) } } } diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 5cb64b1720..4163d86383 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -735,6 +735,7 @@ Appel non chiffré Liste des appels L\'appel est enregistré + Enregistrement sauvegardé %s enregistre l\'appel %s appels %s appels en pause diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9d117bb4c0..89a1a04b26 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -778,6 +778,7 @@ Call is not encrypted Calls list Call is being recorded + Call recording has been saved %s is recording %s calls %s paused calls From c9f052f39cd7783edb2a6ef7d510872cf54e5559 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 3 Apr 2026 15:06:40 +0200 Subject: [PATCH 511/593] Show toast when LDAP server changes have been saved --- .../org/linphone/ui/main/settings/viewmodel/LdapViewModel.kt | 1 + app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 3 files changed, 3 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/LdapViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/LdapViewModel.kt index 8478321a95..9469063cd5 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/LdapViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/LdapViewModel.kt @@ -212,6 +212,7 @@ class LdapViewModel : GenericViewModel() { core.addLdap(ldap) Log.i("$TAG New LDAP config created") } + showGreenToast(R.string.settings_contacts_ldap_saved_toast, R.drawable.check) ldapServerOperationSuccessfulEvent.postValue(Event(true)) } catch (e: Exception) { Log.e("$TAG Exception while creating LDAP: $e") diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4163d86383..0add41d91b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -266,6 +266,7 @@ Attributs de nom Attributs SIP Domaine SIP + Configuration LDAP sauvegardée Une erreur s\'est produite, la configuration LDAP n\'a pas été sauvegardée ! Tous les champs doivent être remplis Réunions diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 89a1a04b26..597d9d55b7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -308,6 +308,7 @@ Name attributes SIP attributes SIP domain + LDAP server saved A error occurred, LDAP server not saved! All fields must be filled Meetings From 498d903548bf6d8a586781975dfe42d130b92025 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 3 Apr 2026 15:13:05 +0200 Subject: [PATCH 512/593] Fixed sharing logs to contact shortcut --- .../chat/fragment/ConversationFragment.kt | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 02935e759a..9198718a89 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -548,6 +548,26 @@ open class ConversationFragment : SlidingPaneChildFragment() { } } } + + sharedViewModel.textToShareFromIntent.observe(viewLifecycleOwner) { text -> + if (text.isNotEmpty() && sharedViewModel.displayedChatRoom != null) { + Log.i("$TAG Found text to share from intent") + sendMessageViewModel.textToSend.value = text + + sharedViewModel.textToShareFromIntent.value = "" + } + } + + sharedViewModel.filesToShareFromIntent.observe(viewLifecycleOwner) { files -> + if (files.isNotEmpty() && sharedViewModel.displayedChatRoom != null) { + Log.i("$TAG Found [${files.size}] files to share from intent") + for (path in files) { + sendMessageViewModel.addAttachments(arrayListOf(path)) + } + + sharedViewModel.filesToShareFromIntent.value = arrayListOf() + } + } } } } @@ -1001,26 +1021,6 @@ open class ConversationFragment : SlidingPaneChildFragment() { } } - sharedViewModel.textToShareFromIntent.observe(viewLifecycleOwner) { text -> - if (text.isNotEmpty() && sharedViewModel.displayedChatRoom != null) { - Log.i("$TAG Found text to share from intent") - sendMessageViewModel.textToSend.value = text - - sharedViewModel.textToShareFromIntent.value = "" - } - } - - sharedViewModel.filesToShareFromIntent.observe(viewLifecycleOwner) { files -> - if (files.isNotEmpty()) { - Log.i("$TAG Found [${files.size}] files to share from intent") - for (path in files) { - sendMessageViewModel.addAttachments(arrayListOf(path)) - } - - sharedViewModel.filesToShareFromIntent.value = arrayListOf() - } - } - sharedViewModel.forceRefreshConversationInfoEvent.observe(viewLifecycleOwner) { it.consume { Log.i("$TAG Force refreshing conversation info") From 039500c7a2bea1bd8d1762de5fcd101b5f000a9d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 3 Apr 2026 15:34:26 +0200 Subject: [PATCH 513/593] Fixed conversation not available in conference/group call when invited to join in --- .../ui/call/conference/viewmodel/ConferenceViewModel.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt index c5a850a282..e028decfcf 100644 --- a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt @@ -292,6 +292,10 @@ class ConferenceViewModel Log.i("$TAG Joined conference already has at least another participant") firstParticipantOtherThanOurselvesJoinedEvent.postValue(Event(true)) } + + val chatEnabled = conference.currentParams.isChatEnabled + isConversationAvailable.postValue(chatEnabled) + Log.i("$TAG Chat is ${if (chatEnabled) "enabled" else "disabled"}") } } } @@ -344,6 +348,7 @@ class ConferenceViewModel val chatEnabled = conference.currentParams.isChatEnabled isConversationAvailable.postValue(chatEnabled) + Log.i("$TAG Chat is ${if (chatEnabled) "enabled" else "disabled"}") val confSubject = conference.subjectUtf8.orEmpty() Log.i( From 6d7a88fd9fb37751513d31b99ee76794e9ac52ad Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 3 Apr 2026 15:06:17 +0000 Subject: [PATCH 514/593] Updated translations from Weblate --- app/src/main/res/values-ca/strings.xml | 4 + app/src/main/res/values-de/strings.xml | 8 +- app/src/main/res/values-fi/strings.xml | 1 + app/src/main/res/values-nl/strings.xml | 6 -- app/src/main/res/values-pt-rBR/strings.xml | 9 +-- app/src/main/res/values-ru/strings.xml | 89 ++++++++++++++++++++-- app/src/main/res/values-sk/strings.xml | 6 -- app/src/main/res/values-uk/strings.xml | 39 ++++++++-- app/src/main/res/values-zh-rCN/strings.xml | 6 -- 9 files changed, 128 insertions(+), 40 deletions(-) diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 068afc9f79..7d3df18770 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -19,4 +19,8 @@ Aquest missatge ha estat esborrat Grup de membres (%s) Esborra-ho per a tothom + Afegeix participants + Afegeix participants + Participants + Afegeix participants diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 5c458ce1a9..a3ec290db0 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -313,7 +313,7 @@ Im Moment kein Kontakt… Favoriten Alle Kontakte - Alles sehen + Alle anzeigen &appName; Kontakte SIP Kontakte Neuer Kontakt @@ -533,7 +533,7 @@ Der Gesprächspartner hat den Anruf beendet Eingehender Anruf für %s Eingehender Videoanruf für %s - Weiterleiten %s an… + %s weiterleiten an… Aktuelle Anrufe Kein andere Anruf Anrufweiterleitung bestätigen @@ -891,8 +891,8 @@ Verbindungsstatus Authentifizierungsinformationen löschen, die nicht mehr mit einem Konto verknüpft sind - %s verwaiste Anmeldeinformation entfernt - %s verwaiste Anmeldeinformationen entfernt + %s nicht mehr benötigte Anmeldedaten wurden entfernt + %s nicht mehr benötigte Anmeldedaten wurden entfernt Klicken Sie auf diese Benachrichtigung, um das Problem zu beheben Ihr Gesprächspartner hört Sie nicht! diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 25e41c7d5d..2af45c3fd2 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -2,4 +2,5 @@ AVPF Todennus tarvitaan + Audio/video kokous, tehdas URI diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 7cdcebc524..eb174c7ce9 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -580,17 +580,11 @@ Kopiëren Downloaden Delen - Geel Toestemming voor meldingen niet verleend! Toestemming om inkomende oproepen te tonen is niet verleend! Overslaan Wachtwoord vergeten? Overslaan - Groen - Blauw - Rood - Roze - Paars Geen resultaat gevonden… Geselecteerde deelnemers zullen hier verschijnen Account(s) verbindingsfout diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 3b66ea56c5..cd8af96a48 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -569,11 +569,6 @@ Baixar Compartilhar Laranja - Amarelo - Azul - Vermelho - Rosa - Roxo Nenhum resultado encontrado… Os participantes selecionados aparecerão aqui Erro de conexão da(s) conta(s) @@ -724,7 +719,6 @@ %s de novas mensagens de voz %s novas mensagens de voz - Verde %s arquivo esperando para ser compartilhado %s de arquivos esperando para ser compartilhados @@ -850,4 +844,7 @@ Limpar contatos importados da agenda nativa Não é possível abrir PDFs protegidos por senha ainda Editar + Entendido + &appName; notificações de erro da conta + Clique nesta notificação para corrigí-la diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 545e24af00..be4bb33a22 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -604,11 +604,6 @@ Копировать Поделиться Оранжевый - Жёлтый - Синий - Красный - Розовый - Фиолетовый Выбранные участники появятся здесь Вы не подключены к интернету Выбранный аккаунт сейчас отключен @@ -767,7 +762,6 @@ Алгоритм аутентификации: %s Пока нет избранного контакта Скачать - Зелёный Обнаружена атака типа «человек посередине» для %s Сообщение было переслано На сегодня встреча не запланирована @@ -855,4 +849,87 @@ Очистить импортированные контакты из встроенной адресной книги Пока не удается открыть PDF-файлы защищенные паролем Участники + Понимаю + Уведомления об ошибках аккаунта + Регистрация аккаунта %s не удалась! + Вы хотите отправить уведомление всем участникам? + Выбор мелодии звонка недоступен! + Не показывать содержимое собщения в уведомлениях Android + Используйте редактор контактов &appName; для родных контактов + Расширенные настройки звонков + Автоматический ответ с включенным видео в обоих направлениях + Очистить информацию об авторизации, которая больше не ассоциирована ни с одним аккаунтом + Отменяет редактирование сообщения + Сообщения не зашифрованы сквозным шифрованием, убедитесь, что вы не делитесь чувствительной информацией! + Достигнут лимит результатов поиска, уточните ваш запрос. + Удалить для меня + Удалить для всех + Вы удалили это сообщение + Участники не найдены + Ответить + Сквозное шифрование через ZRTP + Закрывает меню + Лаванда + Мёд + Бургундский + Мята + Корал + Вишня + Титан + Терракотовый + <отредактировано> + Отсортировать контакты по + Скрыть контакты без SIP-адреса или номера телефона + Подписаться на информацию о присутствии + Эта адресная книга доступна только для чтения + Добавить логи LDAP в журналы &appName; + Включить индикаторы громкости записи/воспроизведения во время звонка + Показать расширенную статистику звонков + Статус соединения + Сменить аккаунт + Незашифрованный диалог + Сообщение редактируется + Отредактировано + ICE: %s + Зашифровано сквозным шифрованием + Не отменять + Включить динамик + Выключить динамик + Откройте &appName; чтобы обновить регистрацию + Ваш собеседник вас не слышит! + Нажмите на уведомление, чтобы это исправить + Проверка номера телефона недоступна + Проверка номера телефона недоступна, пожалуйста, используйте электронную почту для создания аккаунта. + Зарегистрироваться с помощью электронной почты + Алгоритм(ы) LIME (разделены запятыми) + Допустимые значения: c25519, c448, c25519k512, c25519mlk512 and c448mlk1024 + Отключено + Исходящий SIP прокси + Это сообщение было удалено + Встреча будет отменена + Отклонить + Микрофон + HDMI + Отсортировать прошедшие встречи + + %s непривязанная авторизация удалена + %s непривязанных авторизаций удалены + %s непривязанных авторизаций удалены + %s непривязанных авторизаций удалены + + Список совместимых с push-уведомлениями доменов (разделены запятой) + Они будут импортированы снова при следующем запуске приложения, если вы не уберёте разрешение на доступ к контактам + Импортированные контакты были удалены + Этот аккаунт оффлайн, возможно потому что вы сейчас не подключены к интернету. + Если это поле заполнено, исходящий прокси будет включен автоматически. Оставьте его пустым, чтобы отключить исходящий прокси. + Все поля должны быть заполнены + Early-media + Автоматический ответ + Кликните ещё 2 раза, чтобы включить настройки разработчика + Кликните ещё 1 раз, чтобы включить настройки разработчика + Не найдена информация о непривязанной авторизации + Минеральный синий + Сообщения в этом диалоге могут быть перехвачены и прочитаны посторонними людьми, конфиденциальность не гарантирована! + Диапазон IP: %s + Невозможно открыть этот PDF-файл, возможно он повреждён diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index d962a68279..ca3dc739a5 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -705,12 +705,6 @@ Stiahnuť Zdieľať Oranžová - Žltá - Zelená - Modrá - Červená - Ružová - Fialová Žiadne výsledky… Tu sa zobrazia vybraní účastníci Chyba pripojenia účtu(-ov) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index cecedbd2c2..0ecea9cd32 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -166,10 +166,6 @@ Використовувати два сервери Авто URI сервера MWI (індикатор очікування повідомлення) - Зелений - Червоний - Рожевий - Фіолетовий Обрані учасники з\'являться тут %s нове голосове повідомлення @@ -216,7 +212,6 @@ Копіювати Поділитись Помаранчевий - Жовтий Цей обліковий запис онлайн, кожен може вам зателефонувати. Не знайдено… Виберіть режим облікового запису @@ -824,7 +819,6 @@ Втрачені пакети: %s Постквантовий ZRTP Історію видалено - Синій Виклик успішно перенаправлено Сервіс недоступний або помилка мережі Додати до контактів @@ -855,4 +849,37 @@ Учасники Очистити імпортовані контакти з власної адресної книги Редагувати + <відредаговано> + Підписатися на інформацію про присутність + Не скасовувати + Увімкнути динамік + Вимкнути динамік + Відкрийте &appName; щоб оновити реєстрацію + Перевірка номера телефону недоступна + Зареєструватися за допомогою електронної пошти + Алгоритм(и) LIME (розділені комами) + Відключено + Сортувати контакти за + Використовуйте редактор контактів &appName; для рідних контактів + Приховати контакти без SIP-адреси та номера телефону + Ця адресна книга доступна лише для читання + Додати логи LDAP до журналів &appName; + Усі поля мають бути заповнені + Автоматична відповідь з увімкненим відео в обох напрямках + Натисніть ще 1 раз, щоб увімкнути налаштування розробника + Увімкнути індикатори гучності запису/відтворення під час дзвінка + Показати розширену статистику викликів + Зрозуміло + Сповіщення про помилки облікового запису + Натисніть на це сповіщення, щоб виправити це + Ваш співрозмовник вас не чує! + Вибір мелодії дзвінка недоступний! + Не показувати вміст повідомлення в сповіщеннях Android + Розширені налаштування дзвінків + Автоматична відповідь + Натисніть ще 2 рази, щоб увімкнути налаштування розробника + Реєстрація облікового запису %s не вдалася! + Допустимі значення: c25519, c448, c25519k512, c25519mlk512 та c448mlk1024 + Перевірка номера телефону недоступна, будь ласка, скористайтеся електронну пошту для створення облікового запису. + Показати минулі зустрічі diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 8d73c19de5..bbd4eca5ae 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -657,12 +657,6 @@ 复制 下载 分享 - 黄色 - 绿色 - 蓝色 - 红色 - 粉红色 - 紫色 橙色 选定的参与者将显示在此处 账户连接错误 From 26e8cc8a4334669ff2316e456bcff0be586ea39f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 7 Apr 2026 09:09:20 +0000 Subject: [PATCH 515/593] Updated translations from Weblate --- app/src/main/res/values-cs/strings.xml | 3 +++ app/src/main/res/values-de/strings.xml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 81be18514e..5f3dc15973 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -922,4 +922,7 @@ Minerální modrá Smazat to pro mě Smazat to pro všechny + LDAP server uložen + Zastavit nahrávání + Záznam hovoru byl uložen diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a3ec290db0..fe0e22113c 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -914,4 +914,7 @@ Titan Mineralblau Für mich löschen + LDAP Server gespeichert + Aufnahme stoppen + Aufnahme wurde gespeichert From 839dcea67d3c5429cc281944426f4f75ea7dcbf2 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 7 Apr 2026 11:10:49 +0200 Subject: [PATCH 516/593] Bumped version code for next public beta (first release candidate) --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ab97fe4367..57a4a712c1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -106,7 +106,7 @@ android { applicationId = packageName minSdk = 28 targetSdk = 36 - versionCode = 601008 // 6.01.008 + versionCode = 601009 // 6.01.009 versionName = "6.2.0-beta" manifestPlaceholders["appAuthRedirectScheme"] = packageName From 6d51f23e11fe872e2a24612068477cdff9f8040c Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 10 Apr 2026 09:52:36 +0200 Subject: [PATCH 517/593] Remove remote provisioning URI from Core when logging out the last account to prevent possible automatic account configuration when app will be restarted --- .../ui/main/settings/viewmodel/AccountProfileViewModel.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt index 84fd96677c..2f24eefbb8 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt @@ -268,6 +268,14 @@ class AccountProfileViewModel Log.w("$TAG Removing account [$identity] and all related data (auth info, conferences, conversations, call logs)") core.removeAccountWithData(account) accountRemovedEvent.postValue(Event(true)) + + if (core.accountList.isEmpty()) { + Log.w("$TAG No more account found in Core") + if (!core.provisioningUri.isNullOrEmpty()) { + Log.w("$TAG Removing remote provisioning URI") + core.provisioningUri = null + } + } } } } From afe65c51cb5657dcb3d84d3d318b4fe3915eedf3 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 13 Apr 2026 09:39:10 +0200 Subject: [PATCH 518/593] Added DTMFs settings in advanced calls section --- CHANGELOG.md | 1 + .../settings/viewmodel/SettingsViewModel.kt | 25 ++++++++ .../settings_advanced_calls_fragment.xml | 62 ++++++++++++++++++- app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 91 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79b73e8374..9f61dc3ed3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Group changes to describe their impact on the project, as follows: - one to let edit native contacts Linphone copy in-app instead of opening native addressbook third party app - one to show past meetings (they are now hidden by default) - one to hide received message content in android notification + - possibility to choose between RFC2833 and SIP INFO for DTMFs - Added a vu meter for recording & playback volumes (must be enabled in developer settings) - Added support for HDMI audio devices - Added video preview during in-call conversation diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index e3b81a2a94..5e997ee373 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -229,6 +229,8 @@ class SettingsViewModel val mediaEncryptionLabels = arrayListOf() private val mediaEncryptionValues = arrayListOf() val mediaEncryptionMandatory = MutableLiveData() + val rfc2833Dtmf = MutableLiveData() + val sipInfoDtmf = MutableLiveData() val acceptEarlyMedia = MutableLiveData() val ringDuringEarlyMedia = MutableLiveData() val allowOutgoingEarlyMedia = MutableLiveData() @@ -380,6 +382,9 @@ class SettingsViewModel deviceName.postValue(corePreferences.deviceName) remoteProvisioningUrl.postValue(core.provisioningUri) + rfc2833Dtmf.postValue(core.useRfc2833ForDtmf) + sipInfoDtmf.postValue(core.useInfoForDtmf) + acceptEarlyMedia.postValue(corePreferences.acceptEarlyMedia) ringDuringEarlyMedia.postValue(core.ringDuringIncomingEarlyMedia) allowOutgoingEarlyMedia.postValue(corePreferences.allowOutgoingEarlyMedia) @@ -919,6 +924,26 @@ class SettingsViewModel } } + @UiThread + fun toggleRfc2833Dtmf() { + val newValue = rfc2833Dtmf.value == false + + coreContext.postOnCoreThread { core -> + core.useRfc2833ForDtmf = newValue + rfc2833Dtmf.postValue(newValue) + } + } + + @UiThread + fun toggleSipInfoDtmf() { + val newValue = sipInfoDtmf.value == false + + coreContext.postOnCoreThread { core -> + core.useInfoForDtmf = newValue + sipInfoDtmf.postValue(newValue) + } + } + @UiThread fun toggleEarlyMediaExpand() { expandEarlyMedia.value = expandEarlyMedia.value == false diff --git a/app/src/main/res/layout/settings_advanced_calls_fragment.xml b/app/src/main/res/layout/settings_advanced_calls_fragment.xml index e8c1bc348e..57e372050f 100644 --- a/app/src/main/res/layout/settings_advanced_calls_fragment.xml +++ b/app/src/main/res/layout/settings_advanced_calls_fragment.xml @@ -218,7 +218,67 @@ android:layout_marginEnd="16dp" android:checked="@{viewModel.mediaEncryptionMandatory}" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@id/media_encryption" + app:layout_constraintTop_toBottomOf="@id/media_encryption"/> + + + + + + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0add41d91b..86b911a3af 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -226,6 +226,8 @@ Changer de sonnerie Choisissez la sonnerie Le sélectionneur de sonnerie n\'est pas disponible ! + Utiliser la RFC 2833 pour les DTMFs + Utiliser SIP INFO pour les DTMFs Conversations Télécharger automatiquement les fichiers Rendre visible dans la galerie les médias téléchargés diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 597d9d55b7..0aa8608f99 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -268,6 +268,8 @@ Change ringtone Pick ringtone Ringtone picker isn\'t available! + Use RFC 2833 for DTMFs + Use SIP INFO for DTMFs Conversations Auto-download files Make downloaded media public From 0365ebe3ba8ab9cbe85299b1222afa8b12327198 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 13 Apr 2026 09:54:30 +0200 Subject: [PATCH 519/593] Added missing try/catch on PickVisualMediaRequest --- .../main/chat/fragment/ConversationFragment.kt | 16 ++++++++++++---- .../contacts/fragment/EditContactFragment.kt | 7 ++++++- .../main/contacts/fragment/NewContactFragment.kt | 7 ++++++- .../settings/fragment/AccountProfileFragment.kt | 7 ++++++- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 9198718a89..5ab8ef8401 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -693,14 +693,22 @@ open class ConversationFragment : SlidingPaneChildFragment() { binding.setOpenFilePickerClickListener { Log.i("$TAG Opening file picker") - pickDocument.launch(arrayOf("*/*")) + try { + pickDocument.launch(arrayOf("*/*")) + } catch (anfe: ActivityNotFoundException) { + Log.e("$TAG Failed to start file picker: $anfe") + } } binding.setOpenMediaPickerClickListener { Log.i("$TAG Opening media picker") - pickMedia.launch( - PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo) - ) + try { + pickMedia.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageAndVideo) + ) + } catch (anfe: ActivityNotFoundException) { + Log.e("$TAG Failed to start media picker: $anfe") + } } binding.setOpenCameraClickListener { diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/EditContactFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/EditContactFragment.kt index 5391204b27..bfd9c9efe4 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/EditContactFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/EditContactFragment.kt @@ -19,6 +19,7 @@ */ package org.linphone.ui.main.contacts.fragment +import android.content.ActivityNotFoundException import android.content.Context import android.os.Bundle import android.view.LayoutInflater @@ -212,7 +213,11 @@ class EditContactFragment : SlidingPaneChildFragment() { } private fun pickImage() { - pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + try { + pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + } catch (anfe: ActivityNotFoundException) { + Log.e("$TAG Failed to start media picker: $anfe") + } } private fun showAbortConfirmationDialogIfPendingChanges() { diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/NewContactFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/NewContactFragment.kt index 56900ac485..7e357cc1b0 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/NewContactFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/NewContactFragment.kt @@ -19,6 +19,7 @@ */ package org.linphone.ui.main.contacts.fragment +import android.content.ActivityNotFoundException import android.content.Context import android.os.Bundle import android.view.LayoutInflater @@ -214,7 +215,11 @@ class NewContactFragment : GenericMainFragment() { } private fun pickImage() { - pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + try { + pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + } catch (anfe: ActivityNotFoundException) { + Log.e("$TAG Failed to start media picker: $anfe") + } } private fun showAbortConfirmationDialogIfPendingChanges() { diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/AccountProfileFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/AccountProfileFragment.kt index 692d920863..a66eda6310 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/fragment/AccountProfileFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/AccountProfileFragment.kt @@ -19,6 +19,7 @@ */ package org.linphone.ui.main.settings.fragment +import android.content.ActivityNotFoundException import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -223,7 +224,11 @@ class AccountProfileFragment : GenericMainFragment() { } private fun pickImage() { - pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + try { + pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + } catch (anfe: ActivityNotFoundException) { + Log.e("$TAG Failed to start media picker: $anfe") + } } private fun copyAddressToClipboard(value: String) { From c8d2f8161d7a7e3dbbc26837f68f0123f901f2fb Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 13 Apr 2026 15:03:33 +0200 Subject: [PATCH 520/593] Fixed group conversation participants list not expandable if not admin --- app/src/main/res/layout/chat_info_fragment.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/chat_info_fragment.xml b/app/src/main/res/layout/chat_info_fragment.xml index a5a2315f74..1a99e2c425 100644 --- a/app/src/main/res/layout/chat_info_fragment.xml +++ b/app/src/main/res/layout/chat_info_fragment.xml @@ -365,7 +365,7 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:visibility="@{!viewModel.expandParticipants || !viewModel.isMyselfAdmin || !viewModel.isGroup || viewModel.isReadOnly ? View.GONE : View.VISIBLE, default=gone}" + android:visibility="@{viewModel.expandParticipants && viewModel.isGroup && !viewModel.isReadOnly ? View.VISIBLE : View.GONE, default=gone}" android:background="@drawable/shape_squircle_white_background" app:layout_constraintWidth_max="@dimen/section_max_width" app:layout_constraintTop_toBottomOf="@id/participants_label" From 945b0a8ed3db4c027847b5c1fb50ec096eacedce Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 20 Apr 2026 09:40:11 +0200 Subject: [PATCH 521/593] Fixed typo in french translation + bumped AGP to 9.1.1 --- app/src/main/res/values-fr/strings.xml | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 86b911a3af..4a08205ef6 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -597,7 +597,7 @@ Voir le contact Ajouter aux contacts Supprimer l\'historique ? - Tout les messages de cette conversation seront supprimés. + Tous les messages de cette conversation seront supprimés. Historique supprimé %s a rejoint la conversation %s a quitté la conversation diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6d6765c907..283c4e424a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "9.1.0" +agp = "9.1.1" kotlin = "2.3.20" gmsGoogleServices = "4.4.4" firebaseCrashlytics = "3.0.6" From c0552e528f55a76f1476d9f2b255679acfbe1b67 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 24 Apr 2026 09:55:11 +0200 Subject: [PATCH 522/593] Bumped AGP to 9.2.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 283c4e424a..68c5c84a3c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "9.1.1" +agp = "9.2.0" kotlin = "2.3.20" gmsGoogleServices = "4.4.4" firebaseCrashlytics = "3.0.6" From b4f2f18ad1460ea8f71402c3e9bf578c17ae9c5f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 24 Apr 2026 10:19:44 +0200 Subject: [PATCH 523/593] Added missing confirmation dialog when deleting/leaving chat room and deleting call log --- .../chat/fragment/ConversationInfoFragment.kt | 29 +++++ .../fragment/ConversationsListFragment.kt | 57 +++++++++- .../history/fragment/HistoryListFragment.kt | 34 +++++- .../java/org/linphone/utils/DialogUtils.kt | 51 +++++++++ .../main/res/layout/chat_info_fragment.xml | 5 +- .../dialog_leave_group_conversation.xml | 106 ++++++++++++++++++ .../res/layout/dialog_remove_call_log.xml | 106 ++++++++++++++++++ .../res/layout/dialog_remove_conversation.xml | 106 ++++++++++++++++++ app/src/main/res/values-fr/strings.xml | 6 + app/src/main/res/values/strings.xml | 12 +- 10 files changed, 499 insertions(+), 13 deletions(-) create mode 100644 app/src/main/res/layout/dialog_leave_group_conversation.xml create mode 100644 app/src/main/res/layout/dialog_remove_call_log.xml create mode 100644 app/src/main/res/layout/dialog_remove_conversation.xml diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt index fb65ab6626..ef66061257 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt @@ -314,6 +314,10 @@ class ConversationInfoFragment : SlidingPaneChildFragment() { } } + binding.setLeaveGroupClickListener { + showLeaveConfirmationDialog() + } + binding.setDeleteHistoryClickListener { showDeleteHistoryConfirmationDialog() } @@ -486,6 +490,31 @@ class ConversationInfoFragment : SlidingPaneChildFragment() { dialog.show() } + private fun showLeaveConfirmationDialog() { + val dialogModel = ConfirmationDialogModel() + val dialog = DialogUtils.getLeaveConversationConfirmationDialog( + requireActivity(), + dialogModel + ) + + dialogModel.dismissEvent.observe(viewLifecycleOwner) { + it.consume { + dialog.dismiss() + } + } + + dialogModel.confirmEvent.observe(viewLifecycleOwner) { + it.consume { + + Log.i("$TAG Leaving group conversation") + viewModel.leaveGroup() + dialog.dismiss() + } + } + + dialog.show() + } + private fun copyAddressToClipboard(value: String) { if (AppUtils.copyToClipboard(requireContext(), "SIP address", value)) { val message = getString(R.string.sip_address_copied_to_clipboard_toast) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt index d5db5cd232..c631d40e9b 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationsListFragment.kt @@ -42,11 +42,13 @@ import org.linphone.ui.fileviewer.FileViewerActivity import org.linphone.ui.fileviewer.MediaViewerActivity import org.linphone.ui.main.MainActivity.Companion.ARGUMENTS_CONVERSATION_ID import org.linphone.ui.main.chat.adapter.ConversationsListAdapter +import org.linphone.ui.main.chat.model.ConversationModel import org.linphone.ui.main.chat.viewmodel.ConversationsListViewModel import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel import org.linphone.ui.main.fragment.AbstractMainFragment +import org.linphone.utils.ConfirmationDialogModel import org.linphone.utils.DialogUtils import org.linphone.utils.Event import org.linphone.utils.LinphoneUtils @@ -168,12 +170,10 @@ class ConversationsListFragment : AbstractMainFragment() { model.call() }, { // onDeleteConversation - Log.i("$TAG Deleting conversation [${model.id}]") - model.delete() + showDeleteConfirmationDialog(model) }, { // onLeaveGroup - Log.i("$TAG Leaving group conversation [${model.id}]") - model.leaveGroup() + showLeaveConfirmationDialog(model) } ) modalBottomSheet.show(parentFragmentManager, ConversationDialogFragment.TAG) @@ -421,4 +421,53 @@ class ConversationsListFragment : AbstractMainFragment() { dialog.show() } + + private fun showDeleteConfirmationDialog(conversationModel: ConversationModel) { + val dialogModel = ConfirmationDialogModel() + val dialog = DialogUtils.getDeleteConversationConfirmationDialog( + requireActivity(), + dialogModel + ) + + dialogModel.dismissEvent.observe(viewLifecycleOwner) { + it.consume { + dialog.dismiss() + } + } + + dialogModel.confirmEvent.observe(viewLifecycleOwner) { + it.consume { + Log.i("$TAG Deleting conversation [${conversationModel.id}]") + conversationModel.delete() + dialog.dismiss() + } + } + + dialog.show() + } + + private fun showLeaveConfirmationDialog(conversationModel: ConversationModel) { + val dialogModel = ConfirmationDialogModel() + val dialog = DialogUtils.getLeaveConversationConfirmationDialog( + requireActivity(), + dialogModel + ) + + dialogModel.dismissEvent.observe(viewLifecycleOwner) { + it.consume { + dialog.dismiss() + } + } + + dialogModel.confirmEvent.observe(viewLifecycleOwner) { + it.consume { + + Log.i("$TAG Leaving group conversation [${conversationModel.id}]") + conversationModel.leaveGroup() + dialog.dismiss() + } + } + + dialog.show() + } } diff --git a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt index e0cd5b6a3a..1a792a0430 100644 --- a/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/history/fragment/HistoryListFragment.kt @@ -42,6 +42,7 @@ import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel import org.linphone.ui.main.fragment.AbstractMainFragment import org.linphone.ui.main.history.adapter.HistoryListAdapter +import org.linphone.ui.main.history.model.CallLogModel import org.linphone.utils.ConfirmationDialogModel import org.linphone.ui.main.history.viewmodel.HistoryListViewModel import org.linphone.utils.AppUtils @@ -164,9 +165,7 @@ class HistoryListFragment : AbstractMainFragment() { copyNumberOrAddressToClipboard(addressToCopy) }, { // onDeleteCallLog - Log.i("$TAG Deleting call log with ref key or call ID [${model.id}]") - model.delete() - listViewModel.filter() + showDeleteConfirmationDialog(model) } ) modalBottomSheet.show(parentFragmentManager, HistoryMenuDialogFragment.TAG) @@ -286,7 +285,7 @@ class HistoryListFragment : AbstractMainFragment() { } binding.setDeleteAllClickListener { - showDeleteConfirmationDialog() + showDeleteAllConfirmationDialog() } binding.setStartCallClickListener { @@ -339,7 +338,7 @@ class HistoryListFragment : AbstractMainFragment() { } } - private fun showDeleteConfirmationDialog() { + private fun showDeleteAllConfirmationDialog() { val model = ConfirmationDialogModel() val dialog = DialogUtils.getRemoveAllCallLogsConfirmationDialog( requireActivity(), @@ -363,6 +362,31 @@ class HistoryListFragment : AbstractMainFragment() { dialog.show() } + private fun showDeleteConfirmationDialog(callLogModel: CallLogModel) { + val dialogModel = ConfirmationDialogModel() + val dialog = DialogUtils.getRemoveCallLogConfirmationDialog( + requireActivity(), + dialogModel + ) + + dialogModel.dismissEvent.observe(viewLifecycleOwner) { + it.consume { + dialog.dismiss() + } + } + + dialogModel.confirmEvent.observe(viewLifecycleOwner) { + it.consume { + Log.i("$TAG Deleting call log with ref key or call ID [${callLogModel.id}]") + callLogModel.delete() + listViewModel.filter() + dialog.dismiss() + } + } + + dialog.show() + } + private fun showNumbersOrAddressesDialog(list: List) { val numberOrAddressModel = NumberOrAddressPickerDialogModel(list) val dialog = diff --git a/app/src/main/java/org/linphone/utils/DialogUtils.kt b/app/src/main/java/org/linphone/utils/DialogUtils.kt index 5e27ee26c5..d4147aa319 100644 --- a/app/src/main/java/org/linphone/utils/DialogUtils.kt +++ b/app/src/main/java/org/linphone/utils/DialogUtils.kt @@ -67,7 +67,10 @@ import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel import org.linphone.ui.main.model.GroupSetOrEditSubjectDialogModel import androidx.core.graphics.drawable.toDrawable import org.linphone.databinding.DialogAssistantCreateAccountPhoneNumberValidationNotAvailableBinding +import org.linphone.databinding.DialogLeaveGroupConversationBinding import org.linphone.databinding.DialogManageAccountOutboundProxyHelpBinding +import org.linphone.databinding.DialogRemoveCallLogBinding +import org.linphone.databinding.DialogRemoveConversationBinding class DialogUtils { companion object { @@ -279,6 +282,22 @@ class DialogUtils { return getDialog(context, binding) } + @UiThread + fun getRemoveCallLogConfirmationDialog( + context: Context, + viewModel: ConfirmationDialogModel + ): Dialog { + val binding: DialogRemoveCallLogBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.dialog_remove_call_log, + null, + false + ) + binding.viewModel = viewModel + + return getDialog(context, binding) + } + @UiThread fun getRemoveAllCallLogsConfirmationDialog( context: Context, @@ -347,6 +366,38 @@ class DialogUtils { return getDialog(context, binding) } + @UiThread + fun getLeaveConversationConfirmationDialog( + context: Context, + viewModel: ConfirmationDialogModel + ): Dialog { + val binding: DialogLeaveGroupConversationBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.dialog_leave_group_conversation, + null, + false + ) + binding.viewModel = viewModel + + return getDialog(context, binding) + } + + @UiThread + fun getDeleteConversationConfirmationDialog( + context: Context, + viewModel: ConfirmationDialogModel + ): Dialog { + val binding: DialogRemoveConversationBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.dialog_remove_conversation, + null, + false + ) + binding.viewModel = viewModel + + return getDialog(context, binding) + } + @UiThread fun getDeleteConversationHistoryConfirmationDialog( context: Context, diff --git a/app/src/main/res/layout/chat_info_fragment.xml b/app/src/main/res/layout/chat_info_fragment.xml index 1a99e2c425..8fef099476 100644 --- a/app/src/main/res/layout/chat_info_fragment.xml +++ b/app/src/main/res/layout/chat_info_fragment.xml @@ -24,6 +24,9 @@ + @@ -542,7 +545,7 @@ android:layout_marginEnd="16dp" android:background="@{viewModel.isEndToEndEncrypted && !viewModel.isReadOnly ? @drawable/action_background_middle : @drawable/action_background_top, default=@drawable/action_background_middle}" android:drawableStart="@drawable/sign_out" - android:onClick="@{() -> viewModel.leaveGroup()}" + android:onClick="@{leaveGroupClickListener}" android:text="@string/conversation_action_leave_group" android:visibility="@{viewModel.isGroup && !viewModel.isReadOnly ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintWidth_max="@dimen/section_max_width" diff --git a/app/src/main/res/layout/dialog_leave_group_conversation.xml b/app/src/main/res/layout/dialog_leave_group_conversation.xml new file mode 100644 index 0000000000..2fdaf25ad6 --- /dev/null +++ b/app/src/main/res/layout/dialog_leave_group_conversation.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_remove_call_log.xml b/app/src/main/res/layout/dialog_remove_call_log.xml new file mode 100644 index 0000000000..0cd69b8482 --- /dev/null +++ b/app/src/main/res/layout/dialog_remove_call_log.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_remove_conversation.xml b/app/src/main/res/layout/dialog_remove_conversation.xml new file mode 100644 index 0000000000..47a558cf83 --- /dev/null +++ b/app/src/main/res/layout/dialog_remove_conversation.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4a08205ef6..464c353e77 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -426,6 +426,8 @@ L\'ensemble de votre historique d\'appels sera définitivement supprimé. Supprimer l\'historique d\'appels ? L\'ensemble de votre historique d\'appels avec ce correspondant sera définitivement supprimé. + Supprimer cet historique d\'appel ? + Seul cet appel sera supprimée. Aucun contact pour le moment… @@ -598,6 +600,10 @@ Ajouter aux contacts Supprimer l\'historique ? Tous les messages de cette conversation seront supprimés. + Supprimer la conversation ? + Tous les messages de cette conversation seront également supprimés. + Quitter la conversation ? + Vous ne pourrez plus envoyer ni recevoir de nouveaux messages mais l\'historique des messages sera toujours consultable. Historique supprimé %s a rejoint la conversation %s a quitté la conversation diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0aa8608f99..b0652885ee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -466,9 +466,11 @@ Conversation Do you really want to delete all calls history? - All calls will be removed from the history + All calls will be removed from the history. Do you really want to delete the history with that person? - All calls will be removed from the history + All calls will be removed from the history. + Do you really want to delete this call log? + Only this entry will be removed. No contact for the moment… @@ -640,7 +642,11 @@ See contact profile Add to contacts Do you really want to delete all messages? - All messages will be removed from the history + All messages will be removed from the history. + Do you really want to delete this conversation? + All messages will also be removed from the history. + Do you really want to leave this conversation? + You will no longer be able to send or receive new messages, but your message history will still be available. History has been successfully deleted %s joined the conversation %s left the conversation From e8c4fbd152197999052801ddd06e08736c5cb3e2 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 24 Apr 2026 11:08:38 +0200 Subject: [PATCH 524/593] Updated target SDK to 37, bumped dependencies --- app/build.gradle.kts | 4 ++-- .../main/java/org/linphone/telecom/TelecomManager.kt | 3 ++- gradle/libs.versions.toml | 12 ++++++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 57a4a712c1..9854ebc913 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -100,12 +100,12 @@ project.tasks.preBuild.dependsOn("linphoneSdkSource") android { namespace = "org.linphone" - compileSdk = 36 + compileSdk = 37 defaultConfig { applicationId = packageName minSdk = 28 - targetSdk = 36 + targetSdk = 37 versionCode = 601009 // 6.01.009 versionName = "6.2.0-beta" diff --git a/app/src/main/java/org/linphone/telecom/TelecomManager.kt b/app/src/main/java/org/linphone/telecom/TelecomManager.kt index 5f5c1b351f..9b802de6f5 100644 --- a/app/src/main/java/org/linphone/telecom/TelecomManager.kt +++ b/app/src/main/java/org/linphone/telecom/TelecomManager.kt @@ -125,7 +125,8 @@ class TelecomManager uri, direction, type, - capabilities + capabilities, + isLogExcluded = true ) Log.i("$TAG Adding call to Telecom's CallsManager with attributes [$callAttributes]") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 68c5c84a3c..dcda154272 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,18 @@ [versions] agp = "9.2.0" -kotlin = "2.3.20" +kotlin = "2.3.21" gmsGoogleServices = "4.4.4" -firebaseCrashlytics = "3.0.6" -firebaseBomVersion = "34.11.0" +firebaseCrashlytics = "3.0.7" +firebaseBomVersion = "34.12.0" #noinspection NewerVersionAvailable ktlint = "12.3.0" -annotations = "1.9.1" +annotations = "1.10.0" appcompat = "1.7.1" constraintLayout = "2.2.1" coreKtx = "1.18.0" splashscreen = "1.2.0" -telecom = "1.0.1" +telecom = "1.1.0-alpha05" media = "1.7.1" recyclerview = "1.4.0" swipeRefreshLayout = "1.2.0" @@ -20,7 +20,7 @@ slidingpanelayout = "1.2.0" window = "1.5.1" gridlayout = "1.1.0" securityCryptoKtx = "1.1.0" -navigation = "2.9.7" +navigation = "2.9.8" emoji2 = "1.6.0" car = "1.7.0" flexbox = "3.0.0" From 3fe8ac3d4e713ccd5ef68079b28555a177e6ea0a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 24 Apr 2026 11:41:07 +0200 Subject: [PATCH 525/593] Prevent account error notification to show up if push are available --- .../java/org/linphone/notifications/NotificationsManager.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 8b04b41086..0ddeb36948 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -1238,6 +1238,9 @@ class NotificationsManager // the app is put in background and it's not relevant as long as push notifications work if (!corePreferences.keepServiceAlive) return + // Do not notify connexion error in background if account if push notification are available + if (account.params.isPushNotificationAvailable) return + if (Compatibility.isPostNotificationsPermissionGranted(context)) { val pendingIntent = TaskStackBuilder.create(context).run { addNextIntentWithParentStack( From e4ba2bb82f82780cfe9954fda755b81ebfed95e7 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 24 Apr 2026 15:58:01 +0200 Subject: [PATCH 526/593] Fixed SIP address picker not showing up when transferring a call to a contact with more than one SIP address --- .../ui/call/fragment/NewCallFragment.kt | 8 +- .../ui/call/fragment/TransferCallFragment.kt | 73 ++++++++++++++++++- .../model/ContactNumberOrAddressModel.kt | 2 + 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/call/fragment/NewCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/NewCallFragment.kt index fd10c345e1..32fe0b6c2c 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/NewCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/NewCallFragment.kt @@ -80,7 +80,7 @@ class NewCallFragment : GenericCallFragment() { val address = model.address if (address != null) { coreContext.postOnCoreThread { - action(address) + startCall(address) } } } @@ -237,7 +237,7 @@ class NewCallFragment : GenericCallFragment() { coreContext.postOnCoreThread { val friend = model.friend if (friend == null) { - action(model.address) + startCall(model.address) return@postOnCoreThread } @@ -246,7 +246,7 @@ class NewCallFragment : GenericCallFragment() { Log.i( "$TAG Only 1 SIP address or phone number found for contact [${friend.name}], starting call directly" ) - action(singleAvailableAddress) + startCall(singleAvailableAddress) } else { val list = friend.getListOfSipAddressesAndPhoneNumbers(listener) Log.i( @@ -275,7 +275,7 @@ class NewCallFragment : GenericCallFragment() { } @WorkerThread - private fun action(address: Address) { + private fun startCall(address: Address) { Log.i("$TAG Calling [${address.asStringUriOnly()}]") coreContext.startAudioCall(address) diff --git a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt index a2c73f30d6..c03c621f84 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt @@ -25,6 +25,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.UiThread +import androidx.annotation.WorkerThread import androidx.core.view.doOnPreDraw import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.findNavController @@ -35,6 +36,7 @@ import kotlin.getValue import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R +import org.linphone.contacts.getListOfSipAddressesAndPhoneNumbers import org.linphone.core.Address import org.linphone.core.tools.Log import org.linphone.databinding.CallTransferFragmentBinding @@ -43,10 +45,15 @@ import org.linphone.ui.call.model.CallModel import org.linphone.ui.call.viewmodel.CallsViewModel import org.linphone.ui.call.viewmodel.CurrentCallViewModel import org.linphone.ui.main.adapter.ConversationsContactsAndSuggestionsListAdapter +import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener +import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel +import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel import org.linphone.ui.main.history.viewmodel.StartCallViewModel +import org.linphone.ui.main.model.ConversationContactOrSuggestionModel import org.linphone.utils.ConfirmationDialogModel import org.linphone.utils.AppUtils import org.linphone.utils.DialogUtils +import org.linphone.utils.LinphoneUtils import org.linphone.utils.RecyclerViewHeaderDecoration import org.linphone.utils.hideKeyboard import org.linphone.utils.setKeyboardInsetListener @@ -84,6 +91,22 @@ class TransferCallFragment : GenericCallFragment() { private var numberOrAddressPickerDialog: Dialog? = null + private val listener = object : ContactNumberOrAddressClickListener { + @UiThread + override fun onClicked(model: ContactNumberOrAddressModel) { + val address = model.address + if (address != null) { + coreContext.postOnCoreThread { + doCallTransfer(address, model.name) + } + } + } + + @UiThread + override fun onLongPress(model: ContactNumberOrAddressModel) { + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -144,7 +167,7 @@ class TransferCallFragment : GenericCallFragment() { contactsAdapter.onClickedEvent.observe(viewLifecycleOwner) { it.consume { model -> - showConfirmBlindTransferDialog(model.address, model.name) + startCallTransfer(model) } } @@ -269,6 +292,54 @@ class TransferCallFragment : GenericCallFragment() { } } + private fun startCallTransfer(model: ConversationContactOrSuggestionModel) { + coreContext.postOnCoreThread { + val friend = model.friend + if (friend == null) { + doCallTransfer(model.address, model.name) + return@postOnCoreThread + } + + val singleAvailableAddress = LinphoneUtils.getSingleAvailableAddressForFriend(friend) + if (singleAvailableAddress != null) { + Log.i( + "$TAG Only 1 SIP address or phone number found for contact [${friend.name}], starting call directly" + ) + doCallTransfer(singleAvailableAddress, model.name) + } else { + val list = friend.getListOfSipAddressesAndPhoneNumbers(listener) + Log.i( + "$TAG [${list.size}] numbers or addresses found for contact [${friend.name}], showing selection dialog" + ) + + coreContext.postOnMainThread { + val numberOrAddressModel = NumberOrAddressPickerDialogModel(list) + val dialog = + DialogUtils.getNumberOrAddressPickerDialog( + requireActivity(), + numberOrAddressModel + ) + numberOrAddressPickerDialog = dialog + + numberOrAddressModel.dismissEvent.observe(viewLifecycleOwner) { event -> + event.consume { + dialog.dismiss() + } + } + + dialog.show() + } + } + } + } + + @WorkerThread + private fun doCallTransfer(address: Address, name: String) { + coreContext.postOnMainThread { + showConfirmBlindTransferDialog(address, name) + } + } + private fun showConfirmAttendedTransferDialog(callModel: CallModel) { val from = callViewModel.displayedName.value.orEmpty() val to = callModel.displayName.value.orEmpty() diff --git a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactNumberOrAddressModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactNumberOrAddressModel.kt index eeb397e8ff..c4afde6645 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactNumberOrAddressModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactNumberOrAddressModel.kt @@ -39,6 +39,8 @@ class ContactNumberOrAddressModel ) { val selected = MutableLiveData() + val name: String = friend.name.orEmpty() + private var actionDoneCallback: (() -> Unit)? = null @UiThread From bec17a40e03e640cc79dbae48c534b938a6e8bcc Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 24 Apr 2026 16:46:42 +0200 Subject: [PATCH 527/593] Disable delete all history button if history is empty --- app/src/main/res/color/main_top_bar_icon_color.xml | 2 ++ app/src/main/res/layout-land/contacts_list_fragment.xml | 2 +- app/src/main/res/layout-land/history_list_fragment.xml | 3 ++- app/src/main/res/layout-land/main_activity_top_bar.xml | 8 ++++++-- app/src/main/res/layout-land/meetings_list_fragment.xml | 2 +- .../res/layout-sw600dp-land/main_activity_top_bar.xml | 8 ++++++-- app/src/main/res/layout/contacts_list_fragment.xml | 2 +- app/src/main/res/layout/history_list_fragment.xml | 3 ++- app/src/main/res/layout/main_activity_top_bar.xml | 8 ++++++-- app/src/main/res/layout/meetings_list_fragment.xml | 2 +- 10 files changed, 28 insertions(+), 12 deletions(-) diff --git a/app/src/main/res/color/main_top_bar_icon_color.xml b/app/src/main/res/color/main_top_bar_icon_color.xml index f3f3704e88..57692ccc35 100644 --- a/app/src/main/res/color/main_top_bar_icon_color.xml +++ b/app/src/main/res/color/main_top_bar_icon_color.xml @@ -1,5 +1,7 @@ + + @@ -107,7 +110,8 @@ android:layout_height="0dp" android:src="@{extraActionIcon, default=@drawable/dots_three_vertical}" android:contentDescription="@{extraActionContentDescription}" - android:visibility="@{!viewModel.searchBarVisible && enableExtraAction ? View.VISIBLE : View.GONE, default=gone}" + android:enabled="@{extraActionEnabled}" + android:visibility="@{!viewModel.searchBarVisible && showExtraAction ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" /> diff --git a/app/src/main/res/layout-land/meetings_list_fragment.xml b/app/src/main/res/layout-land/meetings_list_fragment.xml index 5b8d7098be..74b640f392 100644 --- a/app/src/main/res/layout-land/meetings_list_fragment.xml +++ b/app/src/main/res/layout-land/meetings_list_fragment.xml @@ -52,7 +52,7 @@ android:id="@+id/top_bar" layout="@layout/main_activity_top_bar" bind:viewModel="@{viewModel}" - bind:enableExtraAction="@{true}" + bind:showExtraAction="@{true}" bind:extraActionIcon="@{@drawable/calendar}" bind:extraActionClickListener="@{todayClickListener}" bind:extraActionContentDescription="@{@string/content_description_meeting_today}" diff --git a/app/src/main/res/layout-sw600dp-land/main_activity_top_bar.xml b/app/src/main/res/layout-sw600dp-land/main_activity_top_bar.xml index a70d7ebcfc..0060fc1c63 100644 --- a/app/src/main/res/layout-sw600dp-land/main_activity_top_bar.xml +++ b/app/src/main/res/layout-sw600dp-land/main_activity_top_bar.xml @@ -5,7 +5,7 @@ + @@ -98,7 +101,8 @@ android:layout_marginEnd="5dp" android:src="@{extraActionIcon, default=@drawable/dots_three_vertical}" android:contentDescription="@{extraActionContentDescription}" - android:visibility="@{!viewModel.searchBarVisible && enableExtraAction ? View.VISIBLE : View.GONE, default=gone}" + android:enabled="@{extraActionEnabled}" + android:visibility="@{!viewModel.searchBarVisible && showExtraAction ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/contacts_list_fragment.xml b/app/src/main/res/layout/contacts_list_fragment.xml index 3c31fa0108..dee9dbe3fc 100644 --- a/app/src/main/res/layout/contacts_list_fragment.xml +++ b/app/src/main/res/layout/contacts_list_fragment.xml @@ -40,7 +40,7 @@ android:id="@+id/top_bar" layout="@layout/main_activity_top_bar" bind:viewModel="@{viewModel}" - bind:enableExtraAction="@{viewModel.showFilter}" + bind:showExtraAction="@{viewModel.showFilter}" bind:extraActionIcon="@{@drawable/funnel}" bind:extraActionClickListener="@{filterClickListener}" bind:extraActionContentDescription="@{@string/content_description_contacts_list_filters}" diff --git a/app/src/main/res/layout/history_list_fragment.xml b/app/src/main/res/layout/history_list_fragment.xml index d7b8e5a4f6..f22dfd9c8f 100644 --- a/app/src/main/res/layout/history_list_fragment.xml +++ b/app/src/main/res/layout/history_list_fragment.xml @@ -46,10 +46,11 @@ android:id="@+id/top_bar" layout="@layout/main_activity_top_bar" bind:viewModel="@{viewModel}" - bind:enableExtraAction="@{true}" + bind:showExtraAction="@{true}" bind:extraActionIcon="@{@drawable/trash_simple}" bind:extraActionClickListener="@{deleteAllClickListener}" bind:extraActionContentDescription="@{@string/menu_delete_history}" + bind:extraActionEnabled="@{!viewModel.callLogs.empty}" android:layout_width="0dp" android:layout_height="@dimen/top_bar_height" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/main_activity_top_bar.xml b/app/src/main/res/layout/main_activity_top_bar.xml index b49f0af09e..d2c657d233 100644 --- a/app/src/main/res/layout/main_activity_top_bar.xml +++ b/app/src/main/res/layout/main_activity_top_bar.xml @@ -6,7 +6,7 @@ + @@ -107,7 +110,8 @@ android:layout_height="0dp" android:src="@{extraActionIcon, default=@drawable/dots_three_vertical}" android:contentDescription="@{extraActionContentDescription}" - android:visibility="@{!viewModel.searchBarVisible && enableExtraAction ? View.VISIBLE : View.GONE, default=gone}" + android:enabled="@{extraActionEnabled}" + android:visibility="@{!viewModel.searchBarVisible && showExtraAction ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" /> diff --git a/app/src/main/res/layout/meetings_list_fragment.xml b/app/src/main/res/layout/meetings_list_fragment.xml index d24b208261..9543a4a11b 100644 --- a/app/src/main/res/layout/meetings_list_fragment.xml +++ b/app/src/main/res/layout/meetings_list_fragment.xml @@ -43,7 +43,7 @@ android:id="@+id/top_bar" layout="@layout/main_activity_top_bar" bind:viewModel="@{viewModel}" - bind:enableExtraAction="@{true}" + bind:showExtraAction="@{true}" bind:extraActionIcon="@{@drawable/calendar}" bind:extraActionClickListener="@{todayClickListener}" bind:extraActionContentDescription="@{@string/content_description_meeting_today}" From 1d7e94fc1b3fc59eee411e6e859ddb18bc65a5d0 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 24 Apr 2026 17:04:42 +0200 Subject: [PATCH 528/593] Fixed extra action disabled default value --- app/src/main/res/layout-land/contacts_list_fragment.xml | 1 + app/src/main/res/layout-land/meetings_list_fragment.xml | 1 + app/src/main/res/layout/contacts_list_fragment.xml | 1 + app/src/main/res/layout/meetings_list_fragment.xml | 1 + 4 files changed, 4 insertions(+) diff --git a/app/src/main/res/layout-land/contacts_list_fragment.xml b/app/src/main/res/layout-land/contacts_list_fragment.xml index f1fe4ad6cb..cbe5db2d28 100644 --- a/app/src/main/res/layout-land/contacts_list_fragment.xml +++ b/app/src/main/res/layout-land/contacts_list_fragment.xml @@ -53,6 +53,7 @@ bind:extraActionIcon="@{@drawable/funnel}" bind:extraActionClickListener="@{filterClickListener}" bind:extraActionContentDescription="@{@string/content_description_contacts_list_filters}" + bind:extraActionEnabled="@{true}" android:layout_width="0dp" android:layout_height="@dimen/top_bar_height" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout-land/meetings_list_fragment.xml b/app/src/main/res/layout-land/meetings_list_fragment.xml index 74b640f392..55e9f1fc1b 100644 --- a/app/src/main/res/layout-land/meetings_list_fragment.xml +++ b/app/src/main/res/layout-land/meetings_list_fragment.xml @@ -56,6 +56,7 @@ bind:extraActionIcon="@{@drawable/calendar}" bind:extraActionClickListener="@{todayClickListener}" bind:extraActionContentDescription="@{@string/content_description_meeting_today}" + bind:extraActionEnabled="@{true}" android:layout_width="0dp" android:layout_height="@dimen/top_bar_height" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/contacts_list_fragment.xml b/app/src/main/res/layout/contacts_list_fragment.xml index dee9dbe3fc..1479911b55 100644 --- a/app/src/main/res/layout/contacts_list_fragment.xml +++ b/app/src/main/res/layout/contacts_list_fragment.xml @@ -44,6 +44,7 @@ bind:extraActionIcon="@{@drawable/funnel}" bind:extraActionClickListener="@{filterClickListener}" bind:extraActionContentDescription="@{@string/content_description_contacts_list_filters}" + bind:extraActionEnabled="@{true}" android:layout_width="0dp" android:layout_height="@dimen/top_bar_height" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/meetings_list_fragment.xml b/app/src/main/res/layout/meetings_list_fragment.xml index 9543a4a11b..f035d46b71 100644 --- a/app/src/main/res/layout/meetings_list_fragment.xml +++ b/app/src/main/res/layout/meetings_list_fragment.xml @@ -47,6 +47,7 @@ bind:extraActionIcon="@{@drawable/calendar}" bind:extraActionClickListener="@{todayClickListener}" bind:extraActionContentDescription="@{@string/content_description_meeting_today}" + bind:extraActionEnabled="@{true}" android:layout_width="0dp" android:layout_height="@dimen/top_bar_height" app:layout_constraintTop_toTopOf="parent" From 45612defbe1758d8275fb3bf2de1e53e5d937026 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Sat, 25 Apr 2026 07:40:21 +0000 Subject: [PATCH 529/593] Updated translations from Weblate --- app/src/main/res/values-cs/strings.xml | 2 + app/src/main/res/values-de/strings.xml | 4 +- app/src/main/res/values-fr/strings.xml | 59 ++------------------------ 3 files changed, 9 insertions(+), 56 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 5f3dc15973..46a7b3af20 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -925,4 +925,6 @@ LDAP server uložen Zastavit nahrávání Záznam hovoru byl uložen + Použít SIP INFO pro DTMF + Použít RFC 2833 pro DTMF diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index fe0e22113c..9933808aa9 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -275,7 +275,7 @@ Push Benachrichtigungen zulassen Push Benachrichtigungen sind nicht verfügbar! IM Verschlüsselung obligatorisch - SIP proxy Server URL + Registrar URI Ausgehender Proxy STUN/TURN Server URL NAT-Richtlinien @@ -917,4 +917,6 @@ LDAP Server gespeichert Aufnahme stoppen Aufnahme wurde gespeichert + RFC 2833 für DTMF-Töne verwenden + SIP INFO für DTMF-Töne verwenden diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 464c353e77..dbc43bd053 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,7 +1,7 @@ - - -]> - + + +]> Adresse SIP @@ -22,12 +22,10 @@ %s jours Rechercher - %s selectionné %s selectionnés - Refuser Accepter @@ -43,7 +41,6 @@ Confirmer J\'ai compris Ne pas annuler - Notifications d\'appels en cours Notifications d\'appels entrants @@ -79,7 +76,6 @@ Votre correspondant ne vous entend pas ! Cliquez sur cette notification pour corriger le problème <contenu masqué> - Bienvenue sur &appName; @@ -88,7 +84,6 @@ Vos communications sont en sécurité grâce au chiffrement de bout en bout. Open source Une application open source et un service gratuit depuis 2001. - Adresse copiée Nouveau compte ajouté @@ -100,7 +95,6 @@ Configuration appliquée Erreur lors du chargement ou de l\'application de la configuration Erreur lors de la création du lecteur média - Conditions de service & politique de confidentialité conditions de service @@ -153,13 +147,11 @@ Un email Un numéro de téléphone Les notifications push ne semblent pas être disponibles sur votre appareil. Celles-ci sont nécessaires à la récupération d’un compte sur l’application mobile avec un numéro de téléphone. - Contacts Appels Conversations Réunions - Mon compte Connecté @@ -170,7 +162,6 @@ Erreur Aucun compte configuré Ajouter un compte - Aide À propos de &appName; @@ -200,7 +191,6 @@ Les journaux ont été nettoyés Échec à l\'envoi des journaux Afficher la configuration - Paramètres Securité @@ -296,7 +286,6 @@ Désactivé Toujours Auto - Paramètres avancés Aider les développeurs à corriger les problèmes en envoyant les logs à Crashlytics après un crash Démarrer au lancement du téléphone @@ -330,7 +319,6 @@ stéréo Codecs vidéo Paramètres Android de &appName; - Paramètres développeurs Afficher les paramètres développeurs Encore 2 clicks pour activer les paramètres développeurs @@ -349,7 +337,6 @@ %s information d\'authentification supprimée %s informations d\'authentification supprimées - Mon compte Détails @@ -380,7 +367,6 @@ Si vous souhaitez supprimer définitivement votre compte rendez-vous sur : https://sip.linphone.org Proxy SIP sortant Si ce champ est rempli, l\'outbound proxy sera activé automatiquement. Laissez-le vide pour le désactiver. - Paramètres de compte Activer les notifications push Notifications push non disponibles @@ -406,12 +392,10 @@ Formater les numéros en utilisant l\'indicatif international Remplacer + par 00 lors du formatage des numéros de téléphone Mettre à jour le mot de passe - Autentification requise La connexion a échoué pour le compte \n%s.\n\nVous pouvez renseigner votre mot de passe à nouveau ou bien vérifier les options de configuration de votre compte. Mot de passe Le compte selectionné est introuvable ! - Nouvel appel Cherchez un contact ou une suggestion Démarrer un appel de groupe @@ -421,14 +405,12 @@ Aucun appel vers/depuis ce compte n\'a été trouvé… Changer de compte Conversation - Supprimer l\'historique d\'appels ? L\'ensemble de votre historique d\'appels sera définitivement supprimé. Supprimer l\'historique d\'appels ? L\'ensemble de votre historique d\'appels avec ce correspondant sera définitivement supprimé. Supprimer cet historique d\'appel ? Seul cet appel sera supprimée. - Aucun contact pour le moment… Aucun contact SIP pour le moment… @@ -439,7 +421,6 @@ Contacts &appName; Contacts SIP Nombre maximal de résultats atteint, affinez votre recherche. - Nouveau contact Modifier contact Prénom @@ -453,7 +434,6 @@ Ignorer les modifications ? Toutes vos modifications seront perdues. Veuillez saisir au moins un nom, prénom ou le nom de l\'entreprise - Coordonnées Entreprise : Poste : @@ -469,7 +449,6 @@ Salut, rejoins moi sur &appName; ! Tu peux le télécharger gratuitement sur %s Contact supprimé Numéro de téléphone copié - Vérifier l\'appareil ? Voulez-vous appeler l\’appareil %2$s de %1$s ? Niveau de confiance @@ -477,7 +456,6 @@ Supprimer %s ? Ce contact sera définitivement supprimé. Choisissez un numéro ou adresse SIP - En ligne En ligne il y a %s En ligne à %s @@ -489,7 +467,6 @@ Appel vidéo Vérifier Appareil sans nom - Aucune conversation liée à ce compte pour le moment… En cours de suppression… @@ -499,7 +476,6 @@ %s fichiers en attente de partage Texte en attente de partage - Marquer comme lu Mettre en sourdine Réactiver les notifications @@ -588,7 +564,6 @@ Vous avez supprimé le message Participants Aucun participant trouvé - Participants (%s) Ajouter des participants Administrateur @@ -613,7 +588,6 @@ Aucune adresse à ajouter au contact Lancer un appel de groupe ? Tous les participants de la conversation recevront un appel. - Vous avez rejoint la conversation Vous avez quitté la conversation %s a rejoint la conversation @@ -630,15 +604,12 @@ Attaque de l\'homme du milieu détectée pour %s Baisse du niveau de sécurité due à %s Nombre maximum de terminaux dépassé pour %s - Médias & documents Médias partagés Documents partagés - Transférer à… Message transféré Transfert annulé - Lu %s Reçu %s Envoyé %s @@ -647,15 +618,12 @@ %1$s %2$s Cliquez pour supprimer Transféré - invitation à une réunion : réunion mise à jour : réunion annulée : message vocal - Aucune réunion aujourd\'hui - Nouvelle réunion Réunion Webinar @@ -692,13 +660,11 @@ Échec de l\'envoi des invitations à la réunion ! Échec de l\'envoi des invitations à certains des participants ! Adresse de la réunion copiée - Rejoindre Annuler Connexion à la réunion Vous allez rejoindre la réunion dans quelques instants… Échec de connexion à la conférence! - Appel sortant Appel entrant @@ -712,7 +678,6 @@ Pas d\'autre appel Confirmer le transfert Vous allez transférer %1$s à %2$s. - Transfert Nouvel appel Liste des appels @@ -752,7 +717,6 @@ Créer une conférence Permission d\'enregistrer l\'audio déclinée ! Permission d\'utliser la caméra déclinée ! - Vérification de sécurité Pour garantir le chiffrement, nous avons besoin d’authentifier l’appareil de votre correspondant.\nVeuillez échanger vos codes : Pour garantir le chiffrement, nous avons besoin de réauthentifier l’appareil de votre correspondant.\nVeuillez ré-échanger vos codes : @@ -762,7 +726,6 @@ Alerte de sécurité Réessayer La confidentialité de votre appel peut être compromise ! - Microphone Oreillette Haut parleur @@ -771,7 +734,6 @@ Casque Écouteurs HDMI - Audio Codec : %s Bande passante : %s @@ -786,7 +748,6 @@ Paquets perdus : %s Paquets réparés : %s Bande passante : %s - Chiffrement du média Chiffrement : %s ZRTP Post Quantique @@ -795,7 +756,6 @@ Algorithme de hachage : %s Algorithme d\'authentification : %s Algorithme SAS : %s - Historique supprimé Appareil authentifié Transfert en cours @@ -807,7 +767,6 @@ Service indisponible ou erreur réseau Délai d\'attente du serveur dépassé Temporairement indisponible - Partager le lien En attente d\'autres participants… @@ -826,23 +785,18 @@ En pause partage son écran Adresse SIP invalide, ajout impossible à la réunion - Mosaïque Intervenant actif Audio uniquement Trop de participants pour l\'affichage mosaïque - Appel de groupe distant Appel de groupe local - Enregistrements Aucun appel enregistré… - Favoris Aucun contact favori - Ajouter aux contacts Voir le contact @@ -859,7 +813,6 @@ Copier le texte Télécharger Partager - Orange Terracotta @@ -871,7 +824,6 @@ Prune Titane Bleu Minéral - Aucun résultat… Les participants selectionnés apparaîtront ici @@ -894,16 +846,13 @@ %s message vocal en attente %s messages vocaux en attente - Passer Mot de passe oublié ou inconnu ? Passer - La réunion a été mise à jour La réunion a été annulée - La confiance a été vérifiée avec tous les appareils du contact Au moins un appareil du contact n\'est pas de confiance ! From a7108afade60aaab0ed63660f41de13dfb0040ee Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Sat, 25 Apr 2026 09:41:24 +0200 Subject: [PATCH 530/593] Bumped version code for next public beta --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9854ebc913..603a058226 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -106,7 +106,7 @@ android { applicationId = packageName minSdk = 28 targetSdk = 37 - versionCode = 601009 // 6.01.009 + versionCode = 601010 // 6.01.010 versionName = "6.2.0-beta" manifestPlaceholders["appAuthRedirectScheme"] = packageName From 085aad6f680e9e7fa4238c36e21f1c09f7b553d4 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Sat, 25 Apr 2026 09:52:22 +0200 Subject: [PATCH 531/593] Fixed typo + updated CHANGELOG --- CHANGELOG.md | 2 ++ app/src/main/res/values-fr/strings.xml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f61dc3ed3..4366cc3eca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Group changes to describe their impact on the project, as follows: - Allow text selection in chat bubble (once long press menu is displayed) ### Changed +- App now targets API level 37 - No longer follow TelecomManager audio endpoint during calls, using our own routing policy - Show matching contacts & suggestions when filtering call history list & conversations list, allowing to quickly call someone without opening the start call/conversation fragment - Join a conference using default layout instead of audio only when clicking on a meeting SIP URI @@ -65,6 +66,7 @@ Group changes to describe their impact on the project, as follows: ### Fixed - Prevent crash & background mode from turning on when doing a remote provisioning if an account is already configured +- Fixed SIP address picker not showing up in call transfer view when selecting a contact with more than 1 SIP address - Copy raw message content instead of modified one when it contains a participant mention ("@username") - Do not apply Crashlytics plugin if it's not enabled diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index dbc43bd053..62a1dd9b18 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -410,7 +410,7 @@ Supprimer l\'historique d\'appels ? L\'ensemble de votre historique d\'appels avec ce correspondant sera définitivement supprimé. Supprimer cet historique d\'appel ? - Seul cet appel sera supprimée. + Seul cet appel sera supprimé. Aucun contact pour le moment… Aucun contact SIP pour le moment… From 3622430889a56cefdf5e59cf7b832e43b34acecb Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 28 Apr 2026 09:20:33 +0200 Subject: [PATCH 532/593] Remove pending Account & AuthInfo when aborting account creation process --- .../RegisterCodeConfirmationFragment.kt | 11 +++++++ .../viewmodel/AccountCreationViewModel.kt | 29 ++++++++++++++----- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterCodeConfirmationFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterCodeConfirmationFragment.kt index fa033c07b2..b58725be6f 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterCodeConfirmationFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterCodeConfirmationFragment.kt @@ -46,6 +46,8 @@ class RegisterCodeConfirmationFragment : GenericFragment() { R.id.assistant_nav_graph ) + private var accountCreated = false + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -70,6 +72,7 @@ class RegisterCodeConfirmationFragment : GenericFragment() { it.consume { val identity = viewModel.username.value.orEmpty() Log.i("$TAG Account [$identity] has been created, leaving assistant") + accountCreated = true requireActivity().finish() } } @@ -95,6 +98,14 @@ class RegisterCodeConfirmationFragment : GenericFragment() { } } + override fun onDestroy() { + super.onDestroy() + if (!accountCreated) { + Log.w("$TAG Account wasn't completely created, remove Account & Auth Info from Core") + viewModel.abortAccountCreation() + } + } + private fun goBack() { findNavController().popBackStack() } diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt index 19ebe95294..a4f5aff7c9 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt @@ -195,14 +195,7 @@ class AccountCreationViewModel } AccountManagerServicesRequest.Type.SendPhoneNumberLinkingCodeBySms -> { Log.e("$TAG Error sending SMS code, clearing auth info & account") - val authInfo = accountCreatedAuthInfo - if (authInfo != null) { - coreContext.core.removeAuthInfo(authInfo) - } - val account = accountCreated - if (account != null) { - coreContext.core.removeAccount(account) - } + removePendingAccount() if (statusCode == 422) { accountCantBeCreatedBySmsEvent.postValue(Event(true)) @@ -212,6 +205,7 @@ class AccountCreationViewModel } } createEnabled.postValue(true) + lockUsernameAndPassword.postValue(false) } } @@ -426,6 +420,25 @@ class AccountCreationViewModel } } + @UiThread + fun abortAccountCreation() { + coreContext.postOnCoreThread { + removePendingAccount() + } + } + + @WorkerThread + private fun removePendingAccount() { + val authInfo = accountCreatedAuthInfo + if (authInfo != null) { + coreContext.core.removeAuthInfo(authInfo) + } + val account = accountCreated + if (account != null) { + coreContext.core.removeAccount(account) + } + } + @WorkerThread private fun sendCodeBySms() { usernameError.postValue("") From c38f79bd40779529cb63837850f72df2691b3f0a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 28 Apr 2026 10:00:56 +0200 Subject: [PATCH 533/593] Prevent being stuck on PermissionsFragment if going back to it by sliding --- .../org/linphone/ui/assistant/fragment/PermissionsFragment.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/PermissionsFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/PermissionsFragment.kt index 3af77bfd03..4fd6d6c847 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/PermissionsFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/PermissionsFragment.kt @@ -182,6 +182,7 @@ class PermissionsFragment : GenericFragment() { if (findNavController().currentDestination?.id == R.id.permissionsFragment) { val action = PermissionsFragmentDirections.actionPermissionsFragmentToLandingFragment() + leaving = false findNavController().navigate(action) } } From 56a48d828ecb11c35698fa79e47521af8f78f546 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 28 Apr 2026 10:04:17 +0200 Subject: [PATCH 534/593] Added bottom margin to meeting toggle to leave room for FaB when keyboard is opened --- app/src/main/res/layout/meeting_edit_fragment.xml | 1 + app/src/main/res/layout/meeting_schedule_fragment.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/app/src/main/res/layout/meeting_edit_fragment.xml b/app/src/main/res/layout/meeting_edit_fragment.xml index 5a8115d2de..a7b5c4945a 100644 --- a/app/src/main/res/layout/meeting_edit_fragment.xml +++ b/app/src/main/res/layout/meeting_edit_fragment.xml @@ -391,6 +391,7 @@ android:layout_marginTop="20dp" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" + android:layout_marginBottom="64dp" android:text="@string/meeting_schedule_send_invitations_title" android:textColor="?attr/color_main2_600" android:textSize="14sp" diff --git a/app/src/main/res/layout/meeting_schedule_fragment.xml b/app/src/main/res/layout/meeting_schedule_fragment.xml index 25c8a781dd..6f0e390901 100644 --- a/app/src/main/res/layout/meeting_schedule_fragment.xml +++ b/app/src/main/res/layout/meeting_schedule_fragment.xml @@ -527,6 +527,7 @@ android:layout_marginTop="20dp" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" + android:layout_marginBottom="64dp" android:text="@string/meeting_schedule_send_invitations_title" android:textColor="?attr/color_main2_600" android:textSize="14sp" From 5affa5b7289c61ce2fae0fd820f0eea715858db5 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 29 Apr 2026 10:18:34 +0200 Subject: [PATCH 535/593] Added missing confirmation dialog when deleting a meeting --- .../meetings/fragment/MeetingsListFragment.kt | 26 ++++- .../java/org/linphone/utils/DialogUtils.kt | 17 +++ .../main/res/layout/dialog_delete_meeting.xml | 106 ++++++++++++++++++ app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 app/src/main/res/layout/dialog_delete_meeting.xml diff --git a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt index 26fa0f712d..704ab7bacc 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt @@ -199,9 +199,7 @@ class MeetingsListFragment : AbstractMainFragment() { Log.i("$TAG Meeting start hasn't started yet and we are the organizer, asking user if it should be cancelled") showCancelMeetingDialog(model) } else { - Log.i("$TAG Deleting meeting [${model.id}]") - model.delete() - listViewModel.filter() + showDeleteMeetingDialog(model) } } ) @@ -349,4 +347,26 @@ class MeetingsListFragment : AbstractMainFragment() { dialog.show() } + + private fun showDeleteMeetingDialog(meetingModel: MeetingModel) { + val model = ConfirmationDialogModel() + val dialog = DialogUtils.getDeleteMeetingDialog(requireContext(), model) + + model.dismissEvent.observe(viewLifecycleOwner) { + it.consume { + dialog.dismiss() + } + } + + model.confirmEvent.observe(viewLifecycleOwner) { + it.consume { + Log.i("$TAG Deleting meeting [${meetingModel.id}]") + meetingModel.delete() + listViewModel.filter() + dialog.dismiss() + } + } + + dialog.show() + } } diff --git a/app/src/main/java/org/linphone/utils/DialogUtils.kt b/app/src/main/java/org/linphone/utils/DialogUtils.kt index d4147aa319..f1c5e4a923 100644 --- a/app/src/main/java/org/linphone/utils/DialogUtils.kt +++ b/app/src/main/java/org/linphone/utils/DialogUtils.kt @@ -67,6 +67,7 @@ import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel import org.linphone.ui.main.model.GroupSetOrEditSubjectDialogModel import androidx.core.graphics.drawable.toDrawable import org.linphone.databinding.DialogAssistantCreateAccountPhoneNumberValidationNotAvailableBinding +import org.linphone.databinding.DialogDeleteMeetingBinding import org.linphone.databinding.DialogLeaveGroupConversationBinding import org.linphone.databinding.DialogManageAccountOutboundProxyHelpBinding import org.linphone.databinding.DialogRemoveCallLogBinding @@ -599,6 +600,22 @@ class DialogUtils { return getDialog(context, binding) } + @UiThread + fun getDeleteMeetingDialog( + context: Context, + viewModel: ConfirmationDialogModel + ): Dialog { + val binding: DialogDeleteMeetingBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.dialog_delete_meeting, + null, + false + ) + binding.viewModel = viewModel + + return getDialog(context, binding) + } + @UiThread private fun getDialog(context: Context, binding: ViewDataBinding): Dialog { val dialog = Dialog(context, R.style.Theme_LinphoneDialog) diff --git a/app/src/main/res/layout/dialog_delete_meeting.xml b/app/src/main/res/layout/dialog_delete_meeting.xml new file mode 100644 index 0000000000..c8d20e218d --- /dev/null +++ b/app/src/main/res/layout/dialog_delete_meeting.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 62a1dd9b18..3c46a495d0 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -648,6 +648,8 @@ Modifier la réunion La réunion va être annulée Voulez-vous envoyer une notification aux participants ? + Supprimer la réunion ? + La réunion sera supprimée de la liste des réunions de cet appareil uniquement. Annuler la réunion Supprimer la réunion Réunion créée diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b0652885ee..341e6c6036 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -723,6 +723,8 @@ Edit meeting Meeting will be cancelled Do you want to send a notification to all participants? + Delete meeting? + This meeting will only be removed from this device meetings list. Cancel meeting Delete meeting Meeting has been created From fd8251c4f02607f1a21f1df45bcb85e2412bcb9d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 30 Apr 2026 11:44:22 +0200 Subject: [PATCH 536/593] Update myselfIsAdmin in chat info --- .../linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt index 7f646e3625..336471f7af 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt @@ -151,6 +151,7 @@ class ConversationInfoViewModel } computeParticipantsList() + isMyselfAdmin.postValue(chatRoom.me?.isAdmin) } @WorkerThread From 4e3e3497ee65bc1a80f7c936c3c02582a5613e59 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 30 Apr 2026 14:42:22 +0200 Subject: [PATCH 537/593] Fixed sharing media/document from conversation media/document list to another conversation --- app/src/main/java/org/linphone/ui/main/MainActivity.kt | 8 ++++---- .../chat/fragment/ConversationDocumentsListFragment.kt | 8 ++++++++ .../main/chat/fragment/ConversationMediaListFragment.kt | 8 ++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/MainActivity.kt b/app/src/main/java/org/linphone/ui/main/MainActivity.kt index f10149c4ff..f1ed190b74 100644 --- a/app/src/main/java/org/linphone/ui/main/MainActivity.kt +++ b/app/src/main/java/org/linphone/ui/main/MainActivity.kt @@ -701,11 +701,11 @@ class MainActivity : GenericActivity() { } if (list.isNotEmpty()) { - sharedViewModel.filesToShareFromIntent.value = list + sharedViewModel.filesToShareFromIntent.postValue(list) } else { if (textToShare.isNotEmpty()) { Log.i("$TAG Found plain text to share") - sharedViewModel.textToShareFromIntent.value = textToShare + sharedViewModel.textToShareFromIntent.postValue(textToShare) } else { Log.w("$TAG Failed to find at least one file or text to share!") } @@ -720,7 +720,7 @@ class MainActivity : GenericActivity() { Log.i( "$TAG Navigating from debug to conversation with ID [$conversationId], computed from shortcut ID" ) - sharedViewModel.showConversationEvent.value = Event(conversationId) + sharedViewModel.showConversationEvent.postValue(Event(conversationId)) } val action = ConversationsListFragmentDirections.actionGlobalConversationsListFragment() @@ -736,7 +736,7 @@ class MainActivity : GenericActivity() { Log.i( "$TAG Navigating to conversation with conversation ID [$conversationId] addresses, computed from shortcut ID" ) - sharedViewModel.showConversationEvent.value = Event(conversationId) + sharedViewModel.showConversationEvent.postValue(Event(conversationId)) } if (findNavController().currentDestination?.id == R.id.conversationsListFragment) { diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt index 028383a8eb..fedd801125 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationDocumentsListFragment.kt @@ -134,6 +134,14 @@ class ConversationDocumentsListFragment : SlidingPaneChildFragment() { } } + sharedViewModel.hideConversationEvent.observe(viewLifecycleOwner) { + it.consume { + Log.w("$TAG We were asked to close conversation, going back") + goBack() + sharedViewModel.hideConversationEvent.postValue(Event(true)) + } + } + scrollListener = object : RecyclerViewScrollListener(layoutManager, 4, true) { @UiThread override fun onLoadMore(totalItemsCount: Int) { diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt index 4a88e1930f..00e35abc42 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationMediaListFragment.kt @@ -163,6 +163,14 @@ class ConversationMediaListFragment : SlidingPaneChildFragment() { } } + sharedViewModel.hideConversationEvent.observe(viewLifecycleOwner) { + it.consume { + Log.w("$TAG We were asked to close conversation, going back") + goBack() + sharedViewModel.hideConversationEvent.postValue(Event(true)) + } + } + scrollListener = object : RecyclerViewScrollListener(layoutManager, spanCount, true) { @UiThread override fun onLoadMore(totalItemsCount: Int) { From 855b4ce40d5cf1a53dde9eb736bea824102400bd Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 30 Apr 2026 15:28:50 +0200 Subject: [PATCH 538/593] Added confirmation dialog before removing participant from group chat --- .../chat/fragment/ConversationInfoFragment.kt | 27 ++++- .../java/org/linphone/utils/DialogUtils.kt | 17 +++ .../dialog_remove_participant_from_group.xml | 106 ++++++++++++++++++ app/src/main/res/values-fr/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 5 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 app/src/main/res/layout/dialog_remove_participant_from_group.xml diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt index ef66061257..48764ecec3 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationInfoFragment.kt @@ -373,8 +373,7 @@ class ConversationInfoFragment : SlidingPaneChildFragment() { popupView.disableAddContact = corePreferences.disableAddContact popupView.setRemoveParticipantClickListener { - Log.i("$TAG Trying to remove participant [$address]") - viewModel.removeParticipant(participantModel) + showConfirmParticipantRemovalPopup(participantModel) popupWindow.dismiss() } @@ -515,6 +514,30 @@ class ConversationInfoFragment : SlidingPaneChildFragment() { dialog.show() } + private fun showConfirmParticipantRemovalPopup(participantModel: ParticipantModel) { + val dialogModel = ConfirmationDialogModel() + val dialog = DialogUtils.getConfirmRemoveParticipantDialog( + requireActivity(), + dialogModel + ) + + dialogModel.dismissEvent.observe(viewLifecycleOwner) { + it.consume { + dialog.dismiss() + } + } + + dialogModel.confirmEvent.observe(viewLifecycleOwner) { + it.consume { + Log.i("$TAG Trying to remove participant [${participantModel.sipUri}]") + viewModel.removeParticipant(participantModel) + dialog.dismiss() + } + } + + dialog.show() + } + private fun copyAddressToClipboard(value: String) { if (AppUtils.copyToClipboard(requireContext(), "SIP address", value)) { val message = getString(R.string.sip_address_copied_to_clipboard_toast) diff --git a/app/src/main/java/org/linphone/utils/DialogUtils.kt b/app/src/main/java/org/linphone/utils/DialogUtils.kt index f1c5e4a923..100da80fc6 100644 --- a/app/src/main/java/org/linphone/utils/DialogUtils.kt +++ b/app/src/main/java/org/linphone/utils/DialogUtils.kt @@ -72,6 +72,7 @@ import org.linphone.databinding.DialogLeaveGroupConversationBinding import org.linphone.databinding.DialogManageAccountOutboundProxyHelpBinding import org.linphone.databinding.DialogRemoveCallLogBinding import org.linphone.databinding.DialogRemoveConversationBinding +import org.linphone.databinding.DialogRemoveParticipantFromGroupBinding class DialogUtils { companion object { @@ -367,6 +368,22 @@ class DialogUtils { return getDialog(context, binding) } + @UiThread + fun getConfirmRemoveParticipantDialog( + context: Context, + viewModel: ConfirmationDialogModel + ): Dialog { + val binding: DialogRemoveParticipantFromGroupBinding = DataBindingUtil.inflate( + LayoutInflater.from(context), + R.layout.dialog_remove_participant_from_group, + null, + false + ) + binding.viewModel = viewModel + + return getDialog(context, binding) + } + @UiThread fun getLeaveConversationConfirmationDialog( context: Context, diff --git a/app/src/main/res/layout/dialog_remove_participant_from_group.xml b/app/src/main/res/layout/dialog_remove_participant_from_group.xml new file mode 100644 index 0000000000..93f0711e6c --- /dev/null +++ b/app/src/main/res/layout/dialog_remove_participant_from_group.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3c46a495d0..8a670f949e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -588,6 +588,8 @@ Aucune adresse à ajouter au contact Lancer un appel de groupe ? Tous les participants de la conversation recevront un appel. + Retirer le participant ? + Ce participant sera retiré de la conversation de groupe. Vous avez rejoint la conversation Vous avez quitté la conversation %s a rejoint la conversation diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 341e6c6036..2418e3a2ae 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -656,6 +656,8 @@ No address to add to contact Start a group call? All participants will receive a call. + Remove participant? + This participant will be removed from the group conversation. You have joined the group You have left the group From cf08f65dc0486244d9da16d3d043781ca9b8aaf1 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 30 Apr 2026 15:50:44 +0200 Subject: [PATCH 539/593] Fixed editing CardDAV synchronized friend list name or URI --- .../ui/main/settings/viewmodel/CardDavViewModel.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/CardDavViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/CardDavViewModel.kt index 6e3d64b8f4..d03b94d4f9 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/CardDavViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/CardDavViewModel.kt @@ -248,6 +248,13 @@ class CardDavViewModel } if (isEdit.value == true && ::friendList.isInitialized) { + friendList.displayName = name + friendList.uri = if (server.startsWith("http://") || server.startsWith("https://")) { + server + } else { + "https://$server" + } + Log.i( "$TAG Changes were made to CardDAV friend list [$name], synchronizing it" ) From 6d5172c36902388bc4a3f06716617da1ae858973 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 30 Apr 2026 17:10:21 +0200 Subject: [PATCH 540/593] Fixed calls started by URI handler aborted due to network reachability --- .../main/java/org/linphone/core/CoreContext.kt | 15 +++++++++------ .../java/org/linphone/ui/main/MainActivity.kt | 3 ++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 8f2468e8ab..69d09778d9 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -875,23 +875,25 @@ class CoreContext fun startAudioCall( address: Address, forceZRTP: Boolean = false, - localAddress: Address? = null + localAddress: Address? = null, + skipNetworkReachabilityTest: Boolean = false ) { val params = core.createCallParams(null) params?.isVideoEnabled = false - startCall(address, params, forceZRTP, localAddress) + startCall(address, params, forceZRTP, localAddress, skipNetworkReachabilityTest) } @WorkerThread fun startVideoCall( address: Address, forceZRTP: Boolean = false, - localAddress: Address? = null + localAddress: Address? = null, + skipNetworkReachabilityTest: Boolean = false ) { val params = core.createCallParams(null) params?.isVideoEnabled = true params?.videoDirection = MediaDirection.SendRecv - startCall(address, params, forceZRTP, localAddress) + startCall(address, params, forceZRTP, localAddress, skipNetworkReachabilityTest) } @WorkerThread @@ -899,9 +901,10 @@ class CoreContext address: Address, callParams: CallParams? = null, forceZRTP: Boolean = false, - localAddress: Address? = null + localAddress: Address? = null, + skipNetworkReachabilityTest: Boolean = false ) { - if (!core.isNetworkReachable) { + if (!skipNetworkReachabilityTest && !core.isNetworkReachable) { Log.e("$TAG Network unreachable, abort outgoing call") return } diff --git a/app/src/main/java/org/linphone/ui/main/MainActivity.kt b/app/src/main/java/org/linphone/ui/main/MainActivity.kt index f1ed190b74..8f02e68b70 100644 --- a/app/src/main/java/org/linphone/ui/main/MainActivity.kt +++ b/app/src/main/java/org/linphone/ui/main/MainActivity.kt @@ -787,7 +787,8 @@ class MainActivity : GenericActivity() { ) Log.i("$TAG Interpreted SIP URI is [${address?.asStringUriOnly()}]") if (address != null) { - coreContext.startAudioCall(address) + // Skip network reachability test, this code will be called too soon + coreContext.startAudioCall(address, skipNetworkReachabilityTest = true) } } } From a4fa6f70a555516bd04d33e20acf9edc43a5e427 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 4 May 2026 10:28:24 +0200 Subject: [PATCH 541/593] No longer using call log startdate as notification ID, found a scenario where it changes --- .../java/org/linphone/notifications/NotificationsManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 0ddeb36948..962b77da83 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -1354,7 +1354,7 @@ class NotificationsManager @WorkerThread private fun getNotificationIdForCall(call: Call): Int { - return call.callLog.startDate.toInt() + return call.hashCode() } @WorkerThread From 08bb27b08a2c024ec837e703aa4af56c38d3b323 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 4 May 2026 10:48:43 +0200 Subject: [PATCH 542/593] Fixed 'scroll to today' button not working if no meetings in the past --- .../linphone/ui/main/meetings/fragment/MeetingsListFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt index 704ab7bacc..20b792cbe9 100644 --- a/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/meetings/fragment/MeetingsListFragment.kt @@ -306,7 +306,7 @@ class MeetingsListFragment : AbstractMainFragment() { } val index = listViewModel.meetings.value.orEmpty().indexOf(todayMeeting) Log.i("$TAG 'Today' is at position [$index]") - if (index > 0) { + if (index >= 0) { binding.meetingsList.smoothScrollToPosition(index) // Workaround to have header decoration visible at top (binding.meetingsList.layoutManager as LinearLayoutManager).scrollToPositionWithOffset( index, From cd867aad28802168548727dacbdf9b4606c09199 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 4 May 2026 09:34:26 +0000 Subject: [PATCH 543/593] Updated translations from Weblate --- app/src/main/res/values-cs/strings.xml | 16 +++++++++++++--- app/src/main/res/values-de/strings.xml | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 46a7b3af20..e2ef6ded89 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -68,7 +68,7 @@ Nenalezena žádná aplikace k otevření tohoto typu souboru Soubor nelze otevřít! Udělit práva administrátora - Z historie budou odebrány všechny zprávy + Z historie budou odebrány všechny zprávy. Zahájit skupinový hovor? Přidal(a) jste se ke skupině Úroveň bezpečnosti byla snížena kvůli %s @@ -130,7 +130,7 @@ Účet byl zakázán, nebudete moci příjímat hovory ani zprávy. Účet se připojuje, prosím počkejte… Připojení se nezdařilo, protože chybí nebo je neplatné ověření účtu\n%s.\n\nMůžete znovu zadat heslo nebo zkontrolovat nastavení účtu v konfiguraci. - Z historie budou odstraněny všechny hovory + Z historie budou odstraněny všechny hovory. Nastavení mizejících zpráv Žádný shodný výsledek… Nenalezena žádná média… @@ -408,7 +408,7 @@ Konverzace Opravdu si přejete smazat veškerou historii hovorů? Opravdu si přejete smazat historii s touto osobou? - Z historie bodou odstraněny všechny hovory + Z historie budou odstraněny všechny hovory. Momentálně žádný kontakt… Oblíbené Všechny kontakty @@ -927,4 +927,14 @@ Záznam hovoru byl uložen Použít SIP INFO pro DTMF Použít RFC 2833 pro DTMF + Odstranit účastníka? + Tento účastník byl odstraněn ze skupinové konverzace. + Smazat schůzku? + Tato schůzka bude odstraněna pouze ze seznamu schůzek tohoto zařízení. + Smazán bude pouze tento záznam. + Opravdu chcete smazat tuto konverzaci? + Všechny zprávy budou také smazány z historie. + Už nebudete moci odesílat ani přijímat nové zprávy, ale vaše historie zpráv zůstane k dispozici. + Opravdu chcete smazat tento záznam hovoru? + Opravdu chcete opustit tuto konverzaci? diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 9933808aa9..d8967b31b8 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -307,9 +307,9 @@ Kein Anruf von/zu diesem Konto gefunden… Chat Möchten Sie wirklich den gesamten Anrufverlauf löschen? - Alle Anrufe werden aus dem Verlauf gelöscht + Alle Anrufe werden aus dem Verlauf gelöscht. Möchten Sie den Verlauf mit dieser Person wirklich löschen? - Alle Anrufe werden aus dem Verlauf gelöscht + Alle Anrufe werden aus dem Verlauf gelöscht. Im Moment kein Kontakt… Favoriten Alle Kontakte @@ -448,7 +448,7 @@ Remove admin rights Kontaktprofil anzeigen Möchten Sie wirklich alle Nachrichten löschen? - Alle Nachrichten werden aus dem Verlauf gelöscht + Alle Nachrichten werden aus dem Verlauf gelöscht. Der Verlauf wurde erfolgreich gelöscht %s hat sich dem Chat angeschlossen %s hat den Chat verlassen @@ -919,4 +919,14 @@ Aufnahme wurde gespeichert RFC 2833 für DTMF-Töne verwenden SIP INFO für DTMF-Töne verwenden + Möchten Sie dieses Anrufprotokoll wirklich löschen? + Nur dieser Eintrag wird entfernt. + Alle Nachrichten werden ebenfalls aus dem Verlauf entfernt. + Sie können keine neuen Nachrichten mehr senden oder empfangen, aber Ihr Nachrichtenverlauf bleibt weiterhin verfügbar. + Möchten Sie diese Konversation wirklich löschen? + Möchten Sie diesen Chat wirklich verlassen? + Teilnehmer entfernen? + Besprechung löschen? + Dieses Meeting wird nur aus der Meetingliste dieses Geräts entfernt. + Dieser Teilnehmer wird aus dem Gruppenchat entfernt. From 87e7373f8a02eab355de653fac197487f6f30f12 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 30 Apr 2026 10:02:15 +0200 Subject: [PATCH 544/593] Configured Core to automatically handle file removal when chat message is deleted --- .../java/org/linphone/core/CoreContext.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 69d09778d9..58c4b06def 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -28,6 +28,7 @@ import android.content.Intent import android.media.AudioDeviceCallback import android.media.AudioDeviceInfo import android.media.AudioManager +import android.os.Environment import android.os.Handler import android.os.HandlerThread import android.os.Looper @@ -668,6 +669,21 @@ class CoreContext Log.i("$TAG Core started, updating configuration if required") core.videoCodecPriorityPolicy = CodecPriorityPolicy.Auto + // Set in the Core the list of directories from which it is allowed to delete a file related to a chat message + val paths = arrayListOf() + if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) { + paths.add(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath.orEmpty()) + paths.add(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)?.absolutePath.orEmpty()) + paths.add(context.getExternalFilesDir(Compatibility.getRecordingsDirectory())?.absolutePath.orEmpty()) + } + paths.add(context.filesDir?.absolutePath.orEmpty()) + val pathsArray = arrayOfNulls(paths.size) + paths.toArray(pathsArray) + core.setChatMessageFilesDirectories(pathsArray) + for (path in paths) { + Log.i("$TAG Adding path [$path] to list of directories from which Core is allowed to delete files from") + } + val currentVersion = BuildConfig.VERSION_CODE val oldVersion = corePreferences.linphoneConfigurationVersion Log.w("$TAG Current configuration version is [$oldVersion]") @@ -681,6 +697,9 @@ class CoreContext disablePushNotificationsFromThirdPartySipAccounts() } else if (oldVersion < 600009) { // 6.0.9 removePortFromSipIdentity() + } else if (oldVersion < 602000) { // 6.2.0 + core.isChatMessageFilesDeletionEnabled = true + Log.i("$TAG Core is allowed to automatically delete files from previously logged directories when a chat message is deleted") } if (core.logCollectionUploadServerUrl.isNullOrEmpty()) { From 84b21f57a35d9d3700da5d409bc0a14e6314520f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 5 May 2026 13:31:58 +0200 Subject: [PATCH 545/593] Bumped version code for next beta --- app/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 603a058226..178ed966e0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -106,7 +106,7 @@ android { applicationId = packageName minSdk = 28 targetSdk = 37 - versionCode = 601010 // 6.01.010 + versionCode = 601011 // 6.01.011 versionName = "6.2.0-beta" manifestPlaceholders["appAuthRedirectScheme"] = packageName From 631f02e2cb179a632568fc0b1116f06a748454cb Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 6 May 2026 13:10:49 +0200 Subject: [PATCH 546/593] Bumped AGP to 9.2.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dcda154272..56cb6d44d4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "9.2.0" +agp = "9.2.1" kotlin = "2.3.21" gmsGoogleServices = "4.4.4" firebaseCrashlytics = "3.0.7" From 09f23ae71c2294f228ca7ef73249a42c12a0c5ae Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 6 May 2026 15:24:38 +0200 Subject: [PATCH 547/593] Added BLE hearing aid to onAudioDevicesAdded --- app/src/main/java/org/linphone/core/CoreContext.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 58c4b06def..412c86225d 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -150,9 +150,10 @@ class CoreContext ) when (device.type) { - AudioDeviceInfo.TYPE_BLUETOOTH_SCO, AudioDeviceInfo.TYPE_BLE_HEADSET, AudioDeviceInfo.TYPE_BLE_SPEAKER, AudioDeviceInfo.TYPE_HEARING_AID -> { + AudioDeviceInfo.TYPE_BLUETOOTH_SCO, AudioDeviceInfo.TYPE_BLE_HEADSET, AudioDeviceInfo.TYPE_BLE_SPEAKER, AudioDeviceInfo.TYPE_HEARING_AID, AudioDeviceInfo.TYPE_BLE_HEARING_AID -> { atLeastOneNewDeviceIsBluetooth = true } + else -> {} } } From 30a8b4529f3f946382a16d4a7685a01f33406988 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 11 May 2026 09:55:50 +0200 Subject: [PATCH 548/593] Hide IMDN status menu for incoming messages --- app/src/main/res/layout/chat_bubble_long_press_menu.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/chat_bubble_long_press_menu.xml b/app/src/main/res/layout/chat_bubble_long_press_menu.xml index 2fb5f209d1..660c469ec2 100644 --- a/app/src/main/res/layout/chat_bubble_long_press_menu.xml +++ b/app/src/main/res/layout/chat_bubble_long_press_menu.xml @@ -124,6 +124,7 @@ android:layout_height="wrap_content" android:text="@string/menu_show_imdn" android:background="@{viewModel.isMessageInError && viewModel.isMessageOutgoing ? @drawable/action_background_middle : @drawable/action_background_top, default=@drawable/action_background_top}" + android:visibility="@{viewModel.isMessageOutgoing ? View.VISIBLE : View.GONE}" android:drawableEnd="@drawable/info"/> From b2fe49060b58f0dd61008ce5c9c388e36a7f270f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 12 May 2026 09:23:40 +0200 Subject: [PATCH 549/593] Prevent being stuck in the Help page while in the Assistant if there's no account configured --- .../linphone/ui/assistant/AssistantActivity.kt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt b/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt index 7f1275c1bb..43036f9e3e 100644 --- a/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt +++ b/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt @@ -22,6 +22,7 @@ package org.linphone.ui.assistant import android.content.pm.PackageManager import android.os.Bundle import android.view.ViewGroup +import androidx.activity.OnBackPressedCallback import androidx.activity.addCallback import androidx.activity.enableEdgeToEdge import androidx.annotation.UiThread @@ -51,6 +52,15 @@ class AssistantActivity : GenericActivity() { private lateinit var binding: AssistantActivityBinding + private val backPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + val navController = binding.assistantNavContainer.findNavController() + if (navController.currentDestination?.id != R.id.landingFragment) { + navController.popBackStack() + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) @@ -75,8 +85,9 @@ class AssistantActivity : GenericActivity() { if (core.accountList.isEmpty()) { Log.i("$TAG No account configured, disabling back gesture") coreContext.postOnMainThread { - // Disable back gesture / button - onBackPressedDispatcher.addCallback { } + // Only allow to navigate back within the assistant nav controller, + // not to leave the AssistantActivity + onBackPressedDispatcher.addCallback(backPressedCallback) } } } From e5e78b732f5a0dd9a8e83e9a6473aa25950c091b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 12 May 2026 09:56:10 +0200 Subject: [PATCH 550/593] Moved account_creator_url to default RC instead of keeping it in the factory --- app/src/main/assets/linphonerc_default | 3 +++ app/src/main/assets/linphonerc_factory | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/assets/linphonerc_default b/app/src/main/assets/linphonerc_default index 389663a9e7..58b675503a 100644 --- a/app/src/main/assets/linphonerc_default +++ b/app/src/main/assets/linphonerc_default @@ -55,4 +55,7 @@ imdn_to_everybody_threshold=1 [ui] contacts_filter=sip.linphone.org +[account_creator] +url=https://subscribe.linphone.org/api/ + ## End of default rc diff --git a/app/src/main/assets/linphonerc_factory b/app/src/main/assets/linphonerc_factory index 9002c427b2..558f8c7f70 100644 --- a/app/src/main/assets/linphonerc_factory +++ b/app/src/main/assets/linphonerc_factory @@ -47,9 +47,6 @@ store_friends=0 [app] record_aware=1 -[account_creator] -url=https://subscribe.linphone.org/api/ - [lime] lime_update_threshold=86400 From 3b6947393383f7f661f70544e874f2278ba49b47 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 12 May 2026 12:49:52 +0200 Subject: [PATCH 551/593] Remove RLS URI from default RC, add it to assistant_linphone_default_values instead + added migration code to remove RLS URI from Core & FriendLists for user who do not have a sip.linphone.org account --- .../assets/assistant_linphone_default_values | 1 + app/src/main/assets/linphonerc_default | 1 - .../java/org/linphone/core/CoreContext.kt | 31 ++++++++++++++++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/app/src/main/assets/assistant_linphone_default_values b/app/src/main/assets/assistant_linphone_default_values index eacc992471..880741ee13 100644 --- a/app/src/main/assets/assistant_linphone_default_values +++ b/app/src/main/assets/assistant_linphone_default_values @@ -31,5 +31,6 @@
srtp 1 + sips:rls@sip.linphone.org
diff --git a/app/src/main/assets/linphonerc_default b/app/src/main/assets/linphonerc_default index 58b675503a..c373d10ac0 100644 --- a/app/src/main/assets/linphonerc_default +++ b/app/src/main/assets/linphonerc_default @@ -12,7 +12,6 @@ media_encryption=none update_presence_model_timestamp_before_publish_expires_refresh=1 use_rfc2833=1 use_info=1 -rls_uri=sips:rls@sip.linphone.org [net] #Because dynamic bitrate adaption can increase bitrate, we must allow "no limit" diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 412c86225d..aa6f02073e 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -699,8 +699,33 @@ class CoreContext } else if (oldVersion < 600009) { // 6.0.9 removePortFromSipIdentity() } else if (oldVersion < 602000) { // 6.2.0 - core.isChatMessageFilesDeletionEnabled = true - Log.i("$TAG Core is allowed to automatically delete files from previously logged directories when a chat message is deleted") + if (!core.isChatMessageFilesDeletionEnabled) { + core.isChatMessageFilesDeletionEnabled = true + Log.i("$TAG Core is allowed to automatically delete files from previously logged directories when a chat message is deleted") + } + + val rlsUri = core.config.getString("sip", "rls_uri", "").orEmpty() + if (rlsUri.isNotEmpty()) { + var accountOnDefaultDomainFound = false + for (account in core.accountList) { + if (account.params.identityAddress?.domain == corePreferences.defaultDomain) { + accountOnDefaultDomainFound = true + break + } + } + if (!accountOnDefaultDomainFound) { + Log.w("$TAG Removing rls_uri from [sip] section & all friend lists to prevent sending SUBSCRIBE") + core.config.setString("sip", "rls_uri", "") + for (friendList in core.friendsLists) { + if (friendList.rlsAddress != null) { + Log.i("$TAG Removing RLS URI from friend list [${friendList.displayName}]") + friendList.rlsAddress = null + } + } + } else { + Log.i("$TAG Keeping RLS URI as an account on the default domain has been found") + } + } } if (core.logCollectionUploadServerUrl.isNullOrEmpty()) { @@ -1160,8 +1185,6 @@ class CoreContext core.setUserAgent(userAgent, sdkUserAgent) } - // Migration between versions related - @WorkerThread private fun removePortFromSipIdentity() { for (account in core.accountList) { From bc61f4299f9721c5ca1c69e21246cdfdf274d5b2 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 12 May 2026 14:12:37 +0200 Subject: [PATCH 552/593] Bumped version code & updated CHANGELOG --- CHANGELOG.md | 5 +++++ app/build.gradle.kts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4366cc3eca..8d6682f5ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Group changes to describe their impact on the project, as follows: - Handle read-only CardDAV address books, disable edit/delete menus for contacts in read-only FriendList - Added swipe/pull to refresh on contacts list of a CardDAV addressbook has been configured to force the synchronization - Show information to user when filtering contacts doesn't show them all and user may have to refine it's search +- Core will now automatically delete from device files that were sent/received in a chat message when it is deleted (because it's ephemeral or has been manually deleted) - Show Android notification when an account goes to failed registration state (only when background mode is enabled) - New settings: - one for user to choose whether to sort contacts by first name or last name @@ -63,10 +64,14 @@ Group changes to describe their impact on the project, as follows: - Permission fragment will only show missing ones - Added more info into StartupListener logs - Updated password forgotten procedure, will use online account manager platform +- Added confirmation dialog before deleting/removing something/someone (contact, meeting, conversation, call log, etc...) +- Delivery information menu when long pressing an incoming chat message has been removed since we no longer send IMDNs to all participants of the group conversation ### Fixed - Prevent crash & background mode from turning on when doing a remote provisioning if an account is already configured +- Prevent app from sending all phone numbers & SIP addresses to sip.linphone.org presence server for long term presence feature if account is not sip.linphone.org - Fixed SIP address picker not showing up in call transfer view when selecting a contact with more than 1 SIP address +- Prevent incoming call notification from staying visible and answer button from doing nothing after answering incoming conference call - Copy raw message content instead of modified one when it contains a participant mention ("@username") - Do not apply Crashlytics plugin if it's not enabled diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 178ed966e0..71c9090635 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -106,7 +106,7 @@ android { applicationId = packageName minSdk = 28 targetSdk = 37 - versionCode = 601011 // 6.01.011 + versionCode = 601012 // 6.01.012 versionName = "6.2.0-beta" manifestPlaceholders["appAuthRedirectScheme"] = packageName From caee3cea11276f901b89ec6db46a5db8e0e61989 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 15 May 2026 10:17:28 +0200 Subject: [PATCH 553/593] Fixed missing goBack() code on SSO's back button --- .../org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt | 4 ++++ app/src/main/res/layout/single_sign_on_fragment.xml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt b/app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt index 91e40d8721..9ff646b48f 100644 --- a/app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt @@ -66,6 +66,10 @@ class SingleSignOnFragment : GenericMainFragment() { binding.viewModel = viewModel observeToastEvents(viewModel) + binding.setBackClickListener { + goBack() + } + viewModel.singleSignOnProcessCompletedEvent.observe(viewLifecycleOwner) { it.consume { Log.i("$TAG Process complete, going back") diff --git a/app/src/main/res/layout/single_sign_on_fragment.xml b/app/src/main/res/layout/single_sign_on_fragment.xml index a0a0194af1..06316bbba4 100644 --- a/app/src/main/res/layout/single_sign_on_fragment.xml +++ b/app/src/main/res/layout/single_sign_on_fragment.xml @@ -4,6 +4,9 @@ + @@ -21,6 +24,7 @@ Date: Mon, 18 May 2026 14:45:08 +0200 Subject: [PATCH 554/593] Added a few logs to QR code scanner --- app/src/main/java/org/linphone/core/CoreContext.kt | 10 +++++++--- .../ui/assistant/fragment/QrCodeScannerFragment.kt | 1 + .../linphone/ui/assistant/viewmodel/QrCodeViewModel.kt | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index aa6f02073e..58d8a14151 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -1304,24 +1304,28 @@ class CoreContext } fun setBackCamera(): Boolean { - for (camera in core.videoDevicesList) { + val list = core.videoDevicesList + for (camera in list) { if (camera.contains("Back")) { - Log.i("TAG Found back facing camera [$camera], using it") + Log.i("$TAG Found back facing camera [$camera], using it") coreContext.core.videoDevice = camera return true } } + Log.i("$TAG Back camera wasn't found in [${list.size}] detected video devices") return false } fun setFrontCamera(): Boolean { - for (camera in core.videoDevicesList) { + val list = core.videoDevicesList + for (camera in list) { if (camera.contains("Front")) { Log.i("$TAG Found front facing camera [$camera], using it") coreContext.core.videoDevice = camera return true } } + Log.i("$TAG Front camera wasn't found in [${list.size}] detected video devices") return false } } diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt index 8a7c49b1ae..cfe8004921 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt @@ -172,6 +172,7 @@ class QrCodeScannerFragment : GenericFragment() { core.nativePreviewWindowId = binding.qrCodePreview core.isQrcodeVideoPreviewEnabled = true core.isVideoPreviewEnabled = true + Log.i("$TAG Video preview with QR scanner enabled") } } } diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt index 643b488483..a187bacd01 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt @@ -121,6 +121,7 @@ class QrCodeViewModel coreContext.postOnCoreThread { core -> // Just in case, on some devices such as Xiaomi Redmi Note 5 // this is required right after granting the CAMERA permission + Log.i("$TAG Reloading video devices, might be needed if CAMERA permission was granted very recently") core.reloadVideoDevices() if (!coreContext.setBackCamera()) { From 8266c69537511798e35887947a666d7e472de9ef Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 18 May 2026 15:48:17 +0200 Subject: [PATCH 555/593] Removed onError event, using directly formattedRedToast one --- .../ui/assistant/viewmodel/QrCodeViewModel.kt | 5 +++++ .../main/sso/fragment/SingleSignOnFragment.kt | 11 ---------- .../sso/viewmodel/SingleSignOnViewModel.kt | 20 ++++++++----------- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt index a187bacd01..452dff940f 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/QrCodeViewModel.kt @@ -105,6 +105,11 @@ class QrCodeViewModel init { coreContext.postOnCoreThread { core -> core.addListener(coreListener) + + val coreGlobalState = core.globalState + if (coreGlobalState != GlobalState.On) { + Log.e("$TAG Core isn't ON (current state is [$coreGlobalState]), video preview won't work!") + } } } diff --git a/app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt b/app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt index 9ff646b48f..b869dcfe52 100644 --- a/app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt @@ -29,10 +29,8 @@ import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.navArgs import net.openid.appauth.AuthorizationException import net.openid.appauth.AuthorizationResponse -import org.linphone.R import org.linphone.core.tools.Log import org.linphone.databinding.SingleSignOnFragmentBinding -import org.linphone.ui.GenericActivity import org.linphone.ui.main.fragment.GenericMainFragment import org.linphone.ui.main.sso.viewmodel.SingleSignOnViewModel @@ -88,15 +86,6 @@ class SingleSignOnFragment : GenericMainFragment() { } } - viewModel.onErrorEvent.observe(viewLifecycleOwner) { - it.consume { errorMessage -> - (requireActivity() as GenericActivity).showRedToast( - errorMessage, - R.drawable.warning_circle - ) - } - } - val serverUrl = args.serverUrl val username = args.username Log.i("$TAG Found server URL [$serverUrl] and username [$username] in args") diff --git a/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt b/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt index 554fb2f7fd..ee25eb044e 100644 --- a/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt @@ -61,10 +61,6 @@ class SingleSignOnViewModel MutableLiveData() } - val onErrorEvent: MutableLiveData> by lazy { - MutableLiveData() - } - private var clientId: String private val redirectUri: String @@ -125,7 +121,7 @@ class SingleSignOnViewModel performRequestToken(resp) } else { Log.e("$TAG Can't perform request token [$ex]") - onErrorEvent.postValue(Event(ex?.errorDescription.orEmpty())) + showFormattedRedToastEvent.postValue(Event(Pair(ex?.errorDescription.orEmpty(), R.drawable.warning_circle))) operationInProgress.value = false } } @@ -141,8 +137,8 @@ class SingleSignOnViewModel Log.e( "$TAG Failed to fetch configuration from issuer [$singleSignOnUrl]: ${ex.errorDescription}" ) - onErrorEvent.postValue( - Event("Failed to fetch configuration from issuer $singleSignOnUrl") + showFormattedRedToastEvent.postValue( + Event(Pair("Failed to fetch configuration from issuer $singleSignOnUrl", R.drawable.warning_circle)) ) operationInProgress.postValue(false) return@RetrieveConfigurationCallback @@ -150,7 +146,7 @@ class SingleSignOnViewModel if (serviceConfiguration == null) { Log.e("$TAG Service configuration is null!") - onErrorEvent.postValue(Event("Service configuration is null")) + showFormattedRedToastEvent.postValue(Event(Pair("Service configuration is null", R.drawable.warning_circle))) operationInProgress.postValue(false) return@RetrieveConfigurationCallback } @@ -212,7 +208,7 @@ class SingleSignOnViewModel Log.e( "$TAG Failed to perform token refresh [$ex], destroying auth_state.json file" ) - onErrorEvent.postValue(Event(ex?.errorDescription.orEmpty())) + showFormattedRedToastEvent.postValue(Event(Pair(ex?.errorDescription.orEmpty(), R.drawable.warning_circle))) operationInProgress.postValue(false) viewModelScope.launch { @@ -257,7 +253,7 @@ class SingleSignOnViewModel storeTokensInAuthInfo() } else { Log.e("$TAG Failed to perform token request [$ex]") - onErrorEvent.postValue(Event(ex?.errorDescription.orEmpty())) + showFormattedRedToastEvent.postValue(Event(Pair(ex?.errorDescription.orEmpty(), R.drawable.warning_circle))) operationInProgress.postValue(false) } } @@ -277,7 +273,7 @@ class SingleSignOnViewModel return AuthState.jsonDeserialize(content) } catch (exception: Exception) { Log.e("$TAG Failed to use serialized AuthState [$exception]") - onErrorEvent.postValue(Event("Failed to read stored AuthState")) + showFormattedRedToastEvent.postValue(Event(Pair("Failed to read stored AuthState", R.drawable.warning_circle))) operationInProgress.postValue(false) } } @@ -357,7 +353,7 @@ class SingleSignOnViewModel val expire = authState.accessTokenExpirationTime if (expire == null) { Log.e("$TAG Access token expiration time is null!") - onErrorEvent.postValue(Event("Invalid access token expiration time")) + showFormattedRedToastEvent.postValue(Event(Pair("Invalid access token expiration time", R.drawable.warning_circle))) operationInProgress.postValue(false) } else { val accessToken = From 75408a42e6059967853f2f2f3b58ba2e42a6bfe5 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 19 May 2026 17:15:54 +0200 Subject: [PATCH 556/593] Added specific error code for wrong SMS confirmation code during account creation --- .../ui/assistant/viewmodel/AccountCreationViewModel.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt index a4f5aff7c9..9c8670b488 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt @@ -201,6 +201,9 @@ class AccountCreationViewModel accountCantBeCreatedBySmsEvent.postValue(Event(true)) } } + AccountManagerServicesRequest.Type.LinkPhoneNumberUsingCode -> { + Log.e("$TAG Wrong confirmation code") + } else -> { } } @@ -432,10 +435,12 @@ class AccountCreationViewModel val authInfo = accountCreatedAuthInfo if (authInfo != null) { coreContext.core.removeAuthInfo(authInfo) + accountCreatedAuthInfo = null } val account = accountCreated if (account != null) { coreContext.core.removeAccount(account) + accountCreated = null } } From 1cbacc84217c1ac1560ac37f106bb4b7060b0087 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 19 May 2026 10:56:19 +0200 Subject: [PATCH 557/593] Fixed SSO webpage not showing up (had to replace fragment by activity) and abort auth in Core if one is pending to prevent being stuck in Configuring state --- app/src/main/AndroidManifest.xml | 6 + .../java/org/linphone/core/CoreContext.kt | 12 ++ .../fragment/QrCodeScannerFragment.kt | 14 +- .../ThirdPartySipAccountLoginFragment.kt | 14 +- .../java/org/linphone/ui/main/MainActivity.kt | 120 +++++++------- .../main/sso/fragment/SingleSignOnFragment.kt | 105 ------------ .../linphone/ui/sso/SingleSignOnActivity.kt | 149 ++++++++++++++++++ .../SingleSignOnViewModel.kt | 80 +++++----- ...agment.xml => single_sign_on_activity.xml} | 16 +- .../res/navigation/assistant_nav_graph.xml | 22 --- .../main/res/navigation/main_nav_graph.xml | 21 --- 11 files changed, 293 insertions(+), 266 deletions(-) delete mode 100644 app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt create mode 100644 app/src/main/java/org/linphone/ui/sso/SingleSignOnActivity.kt rename app/src/main/java/org/linphone/ui/{main/sso/viewmodel => sso}/SingleSignOnViewModel.kt (84%) rename app/src/main/res/layout/{single_sign_on_fragment.xml => single_sign_on_activity.xml} (86%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bd5a3d7af1..d0a6af02ae 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -136,6 +136,12 @@ android:launchMode="singleTask" android:resizeableActivity="true" /> + + { @@ -903,6 +905,16 @@ class CoreContext } } + @AnyThread + fun abortBearerAuthIfAny() { + coreContext.postOnCoreThread { core -> + if (bearerAuthInfoPendingPasswordUpdate != null) { + Log.e("$TAG Aborting bearer authentication") + core.abortAuthentication(bearerAuthInfoPendingPasswordUpdate) + } + } + } + @WorkerThread fun isAddressMyself(address: Address): Boolean { val found = core.accountList.find { diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt index cfe8004921..824940f28c 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt @@ -20,6 +20,7 @@ package org.linphone.ui.assistant.fragment import android.Manifest +import android.content.Intent import android.content.pm.PackageManager import android.os.Bundle import android.view.LayoutInflater @@ -38,7 +39,7 @@ import org.linphone.databinding.AssistantQrCodeScannerFragmentBinding import org.linphone.ui.GenericActivity import org.linphone.ui.GenericFragment import org.linphone.ui.assistant.viewmodel.QrCodeViewModel -import org.linphone.ui.main.sso.fragment.SingleSignOnFragmentDirections +import org.linphone.ui.sso.SingleSignOnActivity @UiThread class QrCodeScannerFragment : GenericFragment() { @@ -109,13 +110,10 @@ class QrCodeScannerFragment : GenericFragment() { Log.i( "$TAG Navigating to Single Sign On Fragment with server URL [$serverUrl] and username [$username]" ) - if (findNavController().currentDestination?.id == R.id.qrCodeScannerFragment) { - val action = SingleSignOnFragmentDirections.actionGlobalSingleSignOnFragment( - serverUrl, - username - ) - findNavController().navigate(action) - } + val intent = Intent(requireContext(), SingleSignOnActivity::class.java) + intent.putExtra(SingleSignOnActivity.INTENT_EXTRA_USERNAME, username) + intent.putExtra(SingleSignOnActivity.INTENT_EXTRA_SERVER_URL, serverUrl) + startActivity(intent) } } diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountLoginFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountLoginFragment.kt index ba33e9fd63..440448832d 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountLoginFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountLoginFragment.kt @@ -20,6 +20,7 @@ package org.linphone.ui.assistant.fragment import android.content.Context +import android.content.Intent import android.os.Bundle import android.telephony.TelephonyManager import android.view.LayoutInflater @@ -40,7 +41,7 @@ import org.linphone.databinding.AssistantThirdPartySipAccountLoginFragmentBindin import org.linphone.ui.GenericActivity import org.linphone.ui.GenericFragment import org.linphone.ui.assistant.viewmodel.ThirdPartySipAccountLoginViewModel -import org.linphone.ui.main.sso.fragment.SingleSignOnFragmentDirections +import org.linphone.ui.sso.SingleSignOnActivity import org.linphone.utils.DialogUtils import org.linphone.utils.PhoneNumberUtils @@ -140,13 +141,10 @@ class ThirdPartySipAccountLoginFragment : GenericFragment() { Log.i( "$TAG Navigating to Single Sign On Fragment with server URL [$serverUrl] and username [$username]" ) - if (findNavController().currentDestination?.id == R.id.thirdPartySipAccountLoginFragment) { - val action = SingleSignOnFragmentDirections.actionGlobalSingleSignOnFragment( - serverUrl, - username - ) - findNavController().navigate(action) - } + val intent = Intent(requireContext(), SingleSignOnActivity::class.java) + intent.putExtra(SingleSignOnActivity.INTENT_EXTRA_USERNAME, username) + intent.putExtra(SingleSignOnActivity.INTENT_EXTRA_SERVER_URL, serverUrl) + startActivity(intent) } } diff --git a/app/src/main/java/org/linphone/ui/main/MainActivity.kt b/app/src/main/java/org/linphone/ui/main/MainActivity.kt index 8f02e68b70..d40d5b95b1 100644 --- a/app/src/main/java/org/linphone/ui/main/MainActivity.kt +++ b/app/src/main/java/org/linphone/ui/main/MainActivity.kt @@ -67,7 +67,6 @@ import org.linphone.ui.GenericActivity import org.linphone.ui.assistant.AssistantActivity import org.linphone.ui.main.chat.fragment.ConversationsListFragmentDirections import org.linphone.utils.PasswordDialogModel -import org.linphone.ui.main.sso.fragment.SingleSignOnFragmentDirections import org.linphone.ui.main.viewmodel.MainViewModel import org.linphone.ui.main.viewmodel.SharedMainViewModel import org.linphone.ui.welcome.WelcomeActivity @@ -77,6 +76,7 @@ import org.linphone.utils.Event import org.linphone.utils.FileUtils import org.linphone.utils.LinphoneUtils import androidx.core.content.edit +import org.linphone.ui.sso.SingleSignOnActivity @UiThread class MainActivity : GenericActivity() { @@ -289,7 +289,6 @@ class MainActivity : GenericActivity() { } } }) - coreContext.bearerAuthenticationRequestedEvent.observe(this) { it.consume { pair -> val serverUrl = pair.first @@ -298,11 +297,10 @@ class MainActivity : GenericActivity() { Log.i( "$TAG Navigating to Single Sign On Fragment with server URL [$serverUrl] and username [$username]" ) - val action = SingleSignOnFragmentDirections.actionGlobalSingleSignOnFragment( - serverUrl, - username - ) - findNavController().navigate(action) + val intent = Intent(this, SingleSignOnActivity::class.java) + intent.putExtra(SingleSignOnActivity.INTENT_EXTRA_USERNAME, username) + intent.putExtra(SingleSignOnActivity.INTENT_EXTRA_SERVER_URL, serverUrl) + startActivity(intent) } } @@ -379,6 +377,29 @@ class MainActivity : GenericActivity() { val projection = it == CarConnection.CONNECTION_TYPE_PROJECTION coreContext.isConnectedToAndroidAuto = projection } + + coreContext.postOnCoreThread { core -> + if (corePreferences.firstLaunch) { + Log.i("$TAG First time Linphone 6.0 has been started, showing Welcome activity") + corePreferences.firstLaunch = false + coreContext.postOnMainThread { + try { + startActivity(Intent(this, WelcomeActivity::class.java)) + } catch (ise: IllegalStateException) { + Log.e("$TAG Can't start activity: $ise") + } + } + } else if (core.accountList.isEmpty()) { + Log.w("$TAG No account found, showing Assistant activity") + coreContext.postOnMainThread { + try { + startActivity(Intent(this, AssistantActivity::class.java)) + } catch (ise: IllegalStateException) { + Log.e("$TAG Can't start activity: $ise") + } + } + } + } } override fun onPostCreate(savedInstanceState: Bundle?) { @@ -578,62 +599,41 @@ class MainActivity : GenericActivity() { private fun handleMainIntent(intent: Intent) { coreContext.postOnCoreThread { core -> - if (corePreferences.firstLaunch) { - Log.i("$TAG First time Linphone 6.0 has been started, showing Welcome activity") - corePreferences.firstLaunch = false - coreContext.postOnMainThread { - try { - startActivity(Intent(this, WelcomeActivity::class.java)) - } catch (ise: IllegalStateException) { - Log.e("$TAG Can't start activity: $ise") - } - } - } else if (core.accountList.isEmpty()) { - Log.w("$TAG No account found, showing Assistant activity") + if (intent.hasExtra(ARGUMENTS_CHAT)) { + Log.i("$TAG Intent has [Chat] extra") coreContext.postOnMainThread { try { - startActivity(Intent(this, AssistantActivity::class.java)) - } catch (ise: IllegalStateException) { - Log.e("$TAG Can't start activity: $ise") - } - } - } else { - if (intent.hasExtra(ARGUMENTS_CHAT)) { - Log.i("$TAG Intent has [Chat] extra") - coreContext.postOnMainThread { - try { - Log.i("$TAG Trying to go to Conversations fragment") - val args = intent.extras - val conversationId = args?.getString(ARGUMENTS_CONVERSATION_ID, "") - if (conversationId.isNullOrEmpty()) { - Log.w("$TAG Found [Chat] extra but no conversation ID!") - } else { - Log.i("$TAG Found [Chat] extra with conversation ID [$conversationId]") - sharedViewModel.showConversationEvent.value = Event(conversationId) - } - args?.clear() - - if (findNavController().currentDestination?.id == R.id.conversationsListFragment) { - Log.w( - "$TAG Current destination is already conversations list, skipping navigation" - ) - } else { - val navOptionsBuilder = NavOptions.Builder() - navOptionsBuilder.setPopUpTo( - findNavController().currentDestination?.id ?: R.id.historyListFragment, - true - ) - navOptionsBuilder.setLaunchSingleTop(true) - val navOptions = navOptionsBuilder.build() - findNavController().navigate( - R.id.conversationsListFragment, - args, - navOptions - ) - } - } catch (ise: IllegalStateException) { - Log.e("$TAG Can't navigate to Conversations fragment: $ise") + Log.i("$TAG Trying to go to Conversations fragment") + val args = intent.extras + val conversationId = args?.getString(ARGUMENTS_CONVERSATION_ID, "") + if (conversationId.isNullOrEmpty()) { + Log.w("$TAG Found [Chat] extra but no conversation ID!") + } else { + Log.i("$TAG Found [Chat] extra with conversation ID [$conversationId]") + sharedViewModel.showConversationEvent.value = Event(conversationId) } + args?.clear() + + if (findNavController().currentDestination?.id == R.id.conversationsListFragment) { + Log.w( + "$TAG Current destination is already conversations list, skipping navigation" + ) + } else { + val navOptionsBuilder = NavOptions.Builder() + navOptionsBuilder.setPopUpTo( + findNavController().currentDestination?.id ?: R.id.historyListFragment, + true + ) + navOptionsBuilder.setLaunchSingleTop(true) + val navOptions = navOptionsBuilder.build() + findNavController().navigate( + R.id.conversationsListFragment, + args, + navOptions + ) + } + } catch (ise: IllegalStateException) { + Log.e("$TAG Can't navigate to Conversations fragment: $ise") } } } diff --git a/app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt b/app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt deleted file mode 100644 index b869dcfe52..0000000000 --- a/app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) 2010-2023 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.ui.main.sso.fragment - -import android.content.ActivityNotFoundException -import android.content.Intent -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.fragment.navArgs -import net.openid.appauth.AuthorizationException -import net.openid.appauth.AuthorizationResponse -import org.linphone.core.tools.Log -import org.linphone.databinding.SingleSignOnFragmentBinding -import org.linphone.ui.main.fragment.GenericMainFragment -import org.linphone.ui.main.sso.viewmodel.SingleSignOnViewModel - -class SingleSignOnFragment : GenericMainFragment() { - companion object { - private const val TAG = "[Single Sign On Fragment]" - - private const val ACTIVITY_RESULT_ID = 666 - } - - private lateinit var binding: SingleSignOnFragmentBinding - - private lateinit var viewModel: SingleSignOnViewModel - - private val args: SingleSignOnFragmentArgs by navArgs() - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = SingleSignOnFragmentBinding.inflate(layoutInflater) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.lifecycleOwner = viewLifecycleOwner - - viewModel = ViewModelProvider(this)[SingleSignOnViewModel::class.java] - binding.viewModel = viewModel - observeToastEvents(viewModel) - - binding.setBackClickListener { - goBack() - } - - viewModel.singleSignOnProcessCompletedEvent.observe(viewLifecycleOwner) { - it.consume { - Log.i("$TAG Process complete, going back") - goBack() - } - } - - viewModel.startAuthIntentEvent.observe(viewLifecycleOwner) { - it.consume { intent -> - Log.i("$TAG Starting auth intent activity") - try { - startActivityForResult(intent, ACTIVITY_RESULT_ID) - } catch (exception: ActivityNotFoundException) { - Log.e("$TAG No activity found to handle intent: $exception") - } - } - } - - val serverUrl = args.serverUrl - val username = args.username - Log.i("$TAG Found server URL [$serverUrl] and username [$username] in args") - viewModel.setUp(serverUrl, username.orEmpty()) - } - - @Deprecated("Deprecated in Java") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == ACTIVITY_RESULT_ID && data != null) { - val resp = AuthorizationResponse.fromIntent(data) - val ex = AuthorizationException.fromIntent(data) - viewModel.processAuthIntentResponse(resp, ex) - } - - super.onActivityResult(requestCode, resultCode, data) - } -} diff --git a/app/src/main/java/org/linphone/ui/sso/SingleSignOnActivity.kt b/app/src/main/java/org/linphone/ui/sso/SingleSignOnActivity.kt new file mode 100644 index 0000000000..3bff188483 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/sso/SingleSignOnActivity.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2010-2026 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.ui.sso + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import androidx.activity.enableEdgeToEdge +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.ViewModelProvider +import net.openid.appauth.AuthorizationException +import net.openid.appauth.AuthorizationResponse +import org.linphone.R +import org.linphone.core.tools.Log +import org.linphone.databinding.SingleSignOnActivityBinding +import org.linphone.ui.GenericActivity +import kotlin.math.max + +class SingleSignOnActivity : GenericActivity() { + companion object { + private const val TAG = "[Single Sign On Activity]" + + private const val ACTIVITY_RESULT_ID = 666 + + const val INTENT_EXTRA_USERNAME = "EXTRA_USERNAME" + const val INTENT_EXTRA_SERVER_URL = "EXTRA_SERVER_URL" + } + + private lateinit var binding: SingleSignOnActivityBinding + + private lateinit var viewModel: SingleSignOnViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + binding = DataBindingUtil.setContentView(this, R.layout.single_sign_on_activity) + binding.lifecycleOwner = this + setUpToastsArea(binding.toastsArea) + + ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val keyboard = windowInsets.getInsets(WindowInsetsCompat.Type.ime()) + v.updatePadding( + insets.left, + insets.top, + insets.right, + max(insets.bottom, keyboard.bottom) + ) + WindowInsetsCompat.CONSUMED + } + + viewModel = ViewModelProvider(this)[SingleSignOnViewModel::class.java] + binding.viewModel = viewModel + observeToastEvents() + + binding.setBackClickListener { + finish() + } + + viewModel.singleSignOnProcessCompletedEvent.observe(this) { + it.consume { + Log.i("$TAG Process complete, going back") + finish() + } + } + + viewModel.startAuthIntentEvent.observe(this) { + it.consume { intent -> + Log.i("$TAG Starting auth intent activity") + try { + startActivityForResult(intent, ACTIVITY_RESULT_ID) + } catch (exception: ActivityNotFoundException) { + Log.e("$TAG No activity found to handle intent: $exception") + } + } + } + + val username = intent.getStringExtra(INTENT_EXTRA_USERNAME).orEmpty() + val serverUrl = intent.getStringExtra(INTENT_EXTRA_SERVER_URL).orEmpty() + Log.i("$TAG Found server URL [$serverUrl] and username [$username] in args") + viewModel.setUp(serverUrl, username) + } + + @Deprecated("Deprecated in Java") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == ACTIVITY_RESULT_ID && data != null) { + val resp = AuthorizationResponse.fromIntent(data) + val ex = AuthorizationException.fromIntent(data) + viewModel.processAuthIntentResponse(resp, ex) + } + + super.onActivityResult(requestCode, resultCode, data) + } + + private fun observeToastEvents() { + viewModel.showRedToastEvent.observe(this) { + it.consume { pair -> + val message = getString(pair.first) + val icon = pair.second + showRedToast(message, icon) + } + } + + viewModel.showFormattedRedToastEvent.observe(this) { + it.consume { pair -> + val message = pair.first + val icon = pair.second + showRedToast(message, icon) + } + } + + viewModel.showGreenToastEvent.observe(this) { + it.consume { pair -> + val message = getString(pair.first) + val icon = pair.second + showGreenToast(message, icon) + } + } + + viewModel.showFormattedGreenToastEvent.observe(this) { + it.consume { pair -> + val message = pair.first + val icon = pair.second + showGreenToast(message, icon) + } + } + } +} diff --git a/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt b/app/src/main/java/org/linphone/ui/sso/SingleSignOnViewModel.kt similarity index 84% rename from app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt rename to app/src/main/java/org/linphone/ui/sso/SingleSignOnViewModel.kt index ee25eb044e..1aec4ae6d7 100644 --- a/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt +++ b/app/src/main/java/org/linphone/ui/sso/SingleSignOnViewModel.kt @@ -1,29 +1,10 @@ -/* - * Copyright (c) 2010-2023 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.ui.main.sso.viewmodel +package org.linphone.ui.sso import android.content.Intent import androidx.annotation.UiThread +import androidx.core.net.toUri import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import java.io.File import kotlinx.coroutines.launch import net.openid.appauth.AuthState import net.openid.appauth.AuthorizationException @@ -42,7 +23,7 @@ import org.linphone.ui.GenericViewModel import org.linphone.utils.Event import org.linphone.utils.FileUtils import org.linphone.utils.TimestampUtils -import androidx.core.net.toUri +import java.io.File class SingleSignOnViewModel @UiThread @@ -53,6 +34,8 @@ class SingleSignOnViewModel val operationInProgress = MutableLiveData() + val errorMessage = MutableLiveData() + val singleSignOnProcessCompletedEvent: MutableLiveData> by lazy { MutableLiveData() } @@ -121,7 +104,15 @@ class SingleSignOnViewModel performRequestToken(resp) } else { Log.e("$TAG Can't perform request token [$ex]") - showFormattedRedToastEvent.postValue(Event(Pair(ex?.errorDescription.orEmpty(), R.drawable.warning_circle))) + errorMessage.postValue(ex?.errorDescription.orEmpty()) + + val file = File(corePreferences.ssoCacheFile) + viewModelScope.launch { + val cache = file.absolutePath + Log.w("$TAG Deleting SSO cache file [$cache] to allow for a new sign-on") + FileUtils.deleteFile(cache) + } + coreContext.abortBearerAuthIfAny() operationInProgress.value = false } } @@ -137,16 +128,16 @@ class SingleSignOnViewModel Log.e( "$TAG Failed to fetch configuration from issuer [$singleSignOnUrl]: ${ex.errorDescription}" ) - showFormattedRedToastEvent.postValue( - Event(Pair("Failed to fetch configuration from issuer $singleSignOnUrl", R.drawable.warning_circle)) - ) + errorMessage.postValue("Failed to fetch configuration from issuer $singleSignOnUrl") + coreContext.abortBearerAuthIfAny() operationInProgress.postValue(false) return@RetrieveConfigurationCallback } if (serviceConfiguration == null) { Log.e("$TAG Service configuration is null!") - showFormattedRedToastEvent.postValue(Event(Pair("Service configuration is null", R.drawable.warning_circle))) + errorMessage.postValue("Service configuration is null") + coreContext.abortBearerAuthIfAny() operationInProgress.postValue(false) return@RetrieveConfigurationCallback } @@ -174,8 +165,10 @@ class SingleSignOnViewModel } val authRequest = authRequestBuilder.build() - authService = AuthorizationService(coreContext.context) + authService = + AuthorizationService(coreContext.context) val authIntent = authService.getAuthorizationRequestIntent(authRequest) + Log.i("$TAG Starting auth using intent [$authIntent]") startAuthIntentEvent.postValue(Event(authIntent)) } ) @@ -186,7 +179,8 @@ class SingleSignOnViewModel operationInProgress.postValue(true) if (::authState.isInitialized) { if (!::authService.isInitialized) { - authService = AuthorizationService(coreContext.context) + authService = + AuthorizationService(coreContext.context) } val authStateJsonFile = File(corePreferences.ssoCacheFile) @@ -208,11 +202,12 @@ class SingleSignOnViewModel Log.e( "$TAG Failed to perform token refresh [$ex], destroying auth_state.json file" ) - showFormattedRedToastEvent.postValue(Event(Pair(ex?.errorDescription.orEmpty(), R.drawable.warning_circle))) + errorMessage.postValue(ex?.errorDescription.orEmpty()) + coreContext.abortBearerAuthIfAny() operationInProgress.postValue(false) viewModelScope.launch { - FileUtils.deleteFile(authStateJsonFile.absolutePath) + FileUtils.Companion.deleteFile(authStateJsonFile.absolutePath) Log.w( "$TAG Previous auth_state.json file deleted, starting single sign on process from scratch" ) @@ -223,7 +218,7 @@ class SingleSignOnViewModel } catch (ise: IllegalStateException) { Log.e("$TAG Illegal state exception, clearing auth state and trying again: $ise") viewModelScope.launch { - FileUtils.deleteFile(authStateJsonFile.absolutePath) + FileUtils.Companion.deleteFile(authStateJsonFile.absolutePath) authState = getAuthState() performRefreshToken() } @@ -253,7 +248,8 @@ class SingleSignOnViewModel storeTokensInAuthInfo() } else { Log.e("$TAG Failed to perform token request [$ex]") - showFormattedRedToastEvent.postValue(Event(Pair(ex?.errorDescription.orEmpty(), R.drawable.warning_circle))) + errorMessage.postValue(ex?.errorDescription.orEmpty()) + coreContext.abortBearerAuthIfAny() operationInProgress.postValue(false) } } @@ -265,7 +261,7 @@ class SingleSignOnViewModel val file = File(corePreferences.ssoCacheFile) if (file.exists()) { Log.i("$TAG Auth state file found, trying to read it") - val content = FileUtils.readFile(file) + val content = FileUtils.Companion.readFile(file) if (content.isNotEmpty()) { Log.i("$TAG Initializing AuthState from local JSON file") Log.d("$TAG Local JSON file contains [$content]") @@ -273,7 +269,8 @@ class SingleSignOnViewModel return AuthState.jsonDeserialize(content) } catch (exception: Exception) { Log.e("$TAG Failed to use serialized AuthState [$exception]") - showFormattedRedToastEvent.postValue(Event(Pair("Failed to read stored AuthState", R.drawable.warning_circle))) + errorMessage.postValue("Failed to read stored AuthState") + coreContext.abortBearerAuthIfAny() operationInProgress.postValue(false) } } @@ -291,7 +288,7 @@ class SingleSignOnViewModel Log.d("$TAG Date to save is [$data]") val file = File(corePreferences.ssoCacheFile) viewModelScope.launch { - if (FileUtils.dumpStringToFile(data, file)) { + if (FileUtils.Companion.dumpStringToFile(data, file)) { Log.i("$TAG Service configuration saved as JSON as [${file.absolutePath}]") } else { Log.i( @@ -316,16 +313,16 @@ class SingleSignOnViewModel Log.w("$TAG Access token is expired") performRefreshToken() } else { - val date = if (TimestampUtils.isToday(expiration, timestampInSecs = false)) { + val date = if (TimestampUtils.Companion.isToday(expiration, timestampInSecs = false)) { "today" } else { - TimestampUtils.toString( + TimestampUtils.Companion.toString( expiration, onlyDate = true, timestampInSecs = false ) } - val time = TimestampUtils.toString(expiration, timestampInSecs = false) + val time = TimestampUtils.Companion.toString(expiration, timestampInSecs = false) Log.i("$TAG Access token expires [$date] [$time]") storeTokensInAuthInfo() } @@ -333,7 +330,7 @@ class SingleSignOnViewModel Log.w("$TAG Access token expiration info not available") val file = File(corePreferences.ssoCacheFile) viewModelScope.launch { - FileUtils.deleteFile(file.absolutePath) + FileUtils.Companion.deleteFile(file.absolutePath) singleSignOn() } } @@ -353,7 +350,8 @@ class SingleSignOnViewModel val expire = authState.accessTokenExpirationTime if (expire == null) { Log.e("$TAG Access token expiration time is null!") - showFormattedRedToastEvent.postValue(Event(Pair("Invalid access token expiration time", R.drawable.warning_circle))) + errorMessage.postValue("Invalid access token expiration time") + coreContext.abortBearerAuthIfAny() operationInProgress.postValue(false) } else { val accessToken = diff --git a/app/src/main/res/layout/single_sign_on_fragment.xml b/app/src/main/res/layout/single_sign_on_activity.xml similarity index 86% rename from app/src/main/res/layout/single_sign_on_fragment.xml rename to app/src/main/res/layout/single_sign_on_activity.xml index 06316bbba4..a30306e1a0 100644 --- a/app/src/main/res/layout/single_sign_on_fragment.xml +++ b/app/src/main/res/layout/single_sign_on_activity.xml @@ -9,7 +9,7 @@ type="View.OnClickListener" /> + type="org.linphone.ui.sso.SingleSignOnViewModel" /> + + - - - - - - - - - - - - - - Date: Thu, 21 May 2026 09:10:08 +0200 Subject: [PATCH 558/593] Bumped dependencies --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 56cb6d44d4..a7836484bb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,7 @@ agp = "9.2.1" kotlin = "2.3.21" gmsGoogleServices = "4.4.4" firebaseCrashlytics = "3.0.7" -firebaseBomVersion = "34.12.0" +firebaseBomVersion = "34.13.0" #noinspection NewerVersionAvailable ktlint = "12.3.0" @@ -13,7 +13,7 @@ constraintLayout = "2.2.1" coreKtx = "1.18.0" splashscreen = "1.2.0" telecom = "1.1.0-alpha05" -media = "1.7.1" +media = "1.8.0" recyclerview = "1.4.0" swipeRefreshLayout = "1.2.0" slidingpanelayout = "1.2.0" @@ -24,7 +24,7 @@ navigation = "2.9.8" emoji2 = "1.6.0" car = "1.7.0" flexbox = "3.0.0" -material = "1.13.0" +material = "1.14.0" #noinspection NewerVersionAvailable protobuf = "3.25.5" coil = "3.4.0" From 662f9a79b1d92734418dbacd21dd1cf0c55e503a Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 22 May 2026 11:22:39 +0200 Subject: [PATCH 559/593] Do not configure conference in Connected state as it may issue a reINVITE --- .../org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 4bb10d33a5..d8c9bdfd7d 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -370,10 +370,8 @@ class CurrentCallViewModel updateCallDuration() if (call.conference != null) { Log.i( - "$TAG Call is in Connected state and conference isn't null, going to conference fragment" + "$TAG Call is in Connected state and conference isn't null, wait for StreamsRunning state to navigate to conference layout" ) - conferenceModel.configureFromCall(call) - goToConferenceEvent.postValue(Event(true)) } else { conferenceModel.destroy() } From 9c91b4ac0d10bc198b6b2a991b139ee32d3aac9b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 25 May 2026 15:19:00 +0200 Subject: [PATCH 560/593] Code cleanup, bumped gradle version --- .../linphone/ui/assistant/AssistantActivity.kt | 1 - .../linphone/ui/call/adapter/CallsListAdapter.kt | 1 - .../org/linphone/ui/sso/SingleSignOnViewModel.kt | 16 ++++++++-------- app/src/main/res/values-cs/strings.xml | 1 - app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-nl/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-sk/strings.xml | 1 - app/src/main/res/values-uk/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 1 - app/src/main/res/values/strings.xml | 1 - gradle/wrapper/gradle-wrapper.properties | 2 +- 14 files changed, 10 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt b/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt index 43036f9e3e..e0699e0cfb 100644 --- a/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt +++ b/app/src/main/java/org/linphone/ui/assistant/AssistantActivity.kt @@ -23,7 +23,6 @@ import android.content.pm.PackageManager import android.os.Bundle import android.view.ViewGroup import androidx.activity.OnBackPressedCallback -import androidx.activity.addCallback import androidx.activity.enableEdgeToEdge import androidx.annotation.UiThread import androidx.core.content.ContextCompat diff --git a/app/src/main/java/org/linphone/ui/call/adapter/CallsListAdapter.kt b/app/src/main/java/org/linphone/ui/call/adapter/CallsListAdapter.kt index e87ede7b4a..f23f323abb 100644 --- a/app/src/main/java/org/linphone/ui/call/adapter/CallsListAdapter.kt +++ b/app/src/main/java/org/linphone/ui/call/adapter/CallsListAdapter.kt @@ -28,7 +28,6 @@ import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView -import org.linphone.BR.showTransferIcon import org.linphone.R import org.linphone.databinding.CallListCellBinding import org.linphone.ui.call.model.CallModel diff --git a/app/src/main/java/org/linphone/ui/sso/SingleSignOnViewModel.kt b/app/src/main/java/org/linphone/ui/sso/SingleSignOnViewModel.kt index 1aec4ae6d7..737c360872 100644 --- a/app/src/main/java/org/linphone/ui/sso/SingleSignOnViewModel.kt +++ b/app/src/main/java/org/linphone/ui/sso/SingleSignOnViewModel.kt @@ -207,7 +207,7 @@ class SingleSignOnViewModel operationInProgress.postValue(false) viewModelScope.launch { - FileUtils.Companion.deleteFile(authStateJsonFile.absolutePath) + FileUtils.deleteFile(authStateJsonFile.absolutePath) Log.w( "$TAG Previous auth_state.json file deleted, starting single sign on process from scratch" ) @@ -218,7 +218,7 @@ class SingleSignOnViewModel } catch (ise: IllegalStateException) { Log.e("$TAG Illegal state exception, clearing auth state and trying again: $ise") viewModelScope.launch { - FileUtils.Companion.deleteFile(authStateJsonFile.absolutePath) + FileUtils.deleteFile(authStateJsonFile.absolutePath) authState = getAuthState() performRefreshToken() } @@ -261,7 +261,7 @@ class SingleSignOnViewModel val file = File(corePreferences.ssoCacheFile) if (file.exists()) { Log.i("$TAG Auth state file found, trying to read it") - val content = FileUtils.Companion.readFile(file) + val content = FileUtils.readFile(file) if (content.isNotEmpty()) { Log.i("$TAG Initializing AuthState from local JSON file") Log.d("$TAG Local JSON file contains [$content]") @@ -288,7 +288,7 @@ class SingleSignOnViewModel Log.d("$TAG Date to save is [$data]") val file = File(corePreferences.ssoCacheFile) viewModelScope.launch { - if (FileUtils.Companion.dumpStringToFile(data, file)) { + if (FileUtils.dumpStringToFile(data, file)) { Log.i("$TAG Service configuration saved as JSON as [${file.absolutePath}]") } else { Log.i( @@ -313,16 +313,16 @@ class SingleSignOnViewModel Log.w("$TAG Access token is expired") performRefreshToken() } else { - val date = if (TimestampUtils.Companion.isToday(expiration, timestampInSecs = false)) { + val date = if (TimestampUtils.isToday(expiration, timestampInSecs = false)) { "today" } else { - TimestampUtils.Companion.toString( + TimestampUtils.toString( expiration, onlyDate = true, timestampInSecs = false ) } - val time = TimestampUtils.Companion.toString(expiration, timestampInSecs = false) + val time = TimestampUtils.toString(expiration, timestampInSecs = false) Log.i("$TAG Access token expires [$date] [$time]") storeTokensInAuthInfo() } @@ -330,7 +330,7 @@ class SingleSignOnViewModel Log.w("$TAG Access token expiration info not available") val file = File(corePreferences.ssoCacheFile) viewModelScope.launch { - FileUtils.Companion.deleteFile(file.absolutePath) + FileUtils.deleteFile(file.absolutePath) singleSignOn() } } diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index e2ef6ded89..aa722bfaa0 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -793,7 +793,6 @@ Otevře výběr emoji Spustí výběr souboru Zapne/vypne ztlumení konverzace - Konverzace se odstraňuje Tato konverzace není zabezpečená Hledat směrem vzhůru Hledat směrem dolů diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index d8967b31b8..9c8b584da0 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -750,7 +750,6 @@ Öffnet den Medien-Picker Klicken Sie hier, um das Thema dieser Unterhaltung zu bearbeiten Stummschalten dieses Chats - Der Chat wird entfernt Dieser Chat ist nicht gesichert Suche nach oben Suche nach unten diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 8a670f949e..b081a382f6 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -923,7 +923,6 @@ Ouvre le selectionneur de fichier Cliquez pour modifier le sujet de la conversation Met ou enlève la sourdine de la conversation - La conversation est en train d\'être supprimée La conversation n\'est pas chiffrée de bout en bout Rechercher vers le haut Rechercher vers le bas diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index eb174c7ce9..da3765fa5b 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -216,7 +216,7 @@ Begin een nieuw gesprek Zoek naar beneden Zoek omhoog - Gesprek wordt verwijderd + Zet geluid aan/uit voor dit gesprek Lang indrukken om voicemail te bellen Spraakberichten zijn beschikbaar diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index cd8af96a48..656f922073 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -637,7 +637,6 @@ Abre o seletor de arquivos Clique para editar o assunto desta conversa Silencia/reativa o som desta conversa - A conversa está sendo removida Esta conversa não é segura Pesquisar abaixo Rolar para hoje diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index be4bb33a22..828c652f31 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -671,7 +671,6 @@ Открывает средство выбора файлов Нажмите, чтобы изменить тему этой беседы Включает/выключает эту беседу - Беседа удаляется Поиск вверх Поиск вниз Начать новую беседу diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index ca3dc739a5..083ebf39be 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -774,7 +774,6 @@ Otvorí výber emoji Otvorí výber súboru Zapne/vypne stlmenie konverzácie - Konverzácia sa odstraňuje Táto konverzácia nie je zabezpečená Hľadať smerom hore Hľadať smerom dole diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 0ecea9cd32..8edfb93b83 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -173,7 +173,6 @@ %s нових голосових повідомлень %s нових голосових повідомлень - Розмова видаляється Основний колір Контакт в мережі Відобразити меню diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index bbd4eca5ae..644473d668 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -730,7 +730,6 @@ 打开文件选择器 单击以编辑此聊天的主题 压制此聊天使其不说话开/关 - 正在删除聊天 停止语音信息录制 开始录制语音信息 在聊天中发送消息 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2418e3a2ae..8e8c9a11d1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1017,7 +1017,6 @@ Opens file picker Click to edit the subject of this conversation Silences on/off this conversation - Conversation is being removed This conversation isn\'t secured Search up Search down diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 80618190c8..fd40011c40 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Jun 22 12:11:25 CEST 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 064910e2d4e18636e50978c286eb9ac43f0965fd Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 26 May 2026 10:09:51 +0200 Subject: [PATCH 561/593] Prevent to display word null in chat message description in case it contains a content without a name --- .../java/org/linphone/ui/main/chat/model/MessageModel.kt | 5 ++++- app/src/main/java/org/linphone/utils/LinphoneUtils.kt | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt index d1e7520405..4a23534b87 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt @@ -577,7 +577,8 @@ class MessageModel private fun downloadContent(model: FileModel, content: Content) { Log.i("$TAG Start downloading content for file [${model.fileName}]") - if (content.filePath.orEmpty().isEmpty()) { + val path = content.filePath.orEmpty() + if (path.isEmpty()) { val contentName = content.name if (contentName != null) { val isImage = FileUtils.isExtensionImage(contentName) @@ -593,6 +594,8 @@ class MessageModel } else { Log.e("$TAG Content name is null, can't download it!") } + } else { + Log.e("$TAG We already have a file path [$path] for this content, doing nothing") } } diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 32bcd6c37e..33c5c815b8 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -652,7 +652,7 @@ class LinphoneUtils { @WorkerThread private fun getTextDescribingMessage(message: ChatMessage): Pair { - // Check if message is empty (when deleted by it's sender, for everyone) + // Check if message is empty (when deleted by its sender, for everyone) if (message.isRetracted) { val text = if (message.isOutgoing) { AppUtils.getString(R.string.conversation_message_content_deleted_by_us_label) @@ -663,7 +663,7 @@ class LinphoneUtils { } // If message contains text, then use that - var text = message.contents.find { content -> content.isText }?.utf8Text ?: "" + var text = message.contents.find { content -> content.isText }?.utf8Text.orEmpty() var contentDescription = "" if (text.isEmpty()) { @@ -711,7 +711,9 @@ class LinphoneUtils { if (text.isNotEmpty()) { text += ", " } - text += content.name + if (!content.name.isNullOrEmpty()) { + text += content.name + } } } } From 4aea9eefe88b56a76ef41db031488c08bd3dcb22 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 26 May 2026 10:17:43 +0200 Subject: [PATCH 562/593] Improved how we compute the icon in the conversations list last message preview --- .../ui/main/chat/model/ConversationModel.kt | 51 +++++++++++-------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt index 29e7a097fd..b9f11eddb9 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ConversationModel.kt @@ -33,6 +33,7 @@ import org.linphone.core.ChatMessageListenerStub import org.linphone.core.ChatRoom import org.linphone.core.ChatRoom.Capabilities import org.linphone.core.ChatRoomListenerStub +import org.linphone.core.Content import org.linphone.core.EventLog import org.linphone.core.Friend import org.linphone.core.tools.Log @@ -332,27 +333,10 @@ class ConversationModel } else if (message.isForward) { lastMessageContentIcon.postValue(R.drawable.forward) } else { - val firstContent = message.contents.firstOrNull() - val icon = if (firstContent?.isIcalendar == true) { - R.drawable.calendar - } else if (firstContent?.isVoiceRecording == true) { - R.drawable.waveform - } else if (firstContent?.isFile == true) { - val mime = "${firstContent.type}/${firstContent.subtype}" - val mimeType = FileUtils.getMimeType(mime) - val drawable = when (mimeType) { - FileUtils.MimeType.Image -> R.drawable.file_image - FileUtils.MimeType.Video -> R.drawable.file_video - FileUtils.MimeType.Audio -> R.drawable.file_audio - FileUtils.MimeType.Pdf -> R.drawable.file_pdf - FileUtils.MimeType.PlainText -> R.drawable.file_text - else -> R.drawable.file - } - drawable - } else if (firstContent?.isFileTransfer == true) { - R.drawable.download_simple - } else { - 0 + var icon = 0 + for (content in message.contents) { + icon = getIconFromContent(content) + if (icon != 0) break } lastMessageContentIcon.postValue(icon) } @@ -398,6 +382,31 @@ class ConversationModel } } + @WorkerThread + private fun getIconFromContent(content: Content): Int { + return if (content.isIcalendar) { + R.drawable.calendar + } else if (content.isVoiceRecording) { + R.drawable.waveform + } else if (content.isFile) { + val mime = "${content.type}/${content.subtype}" + val mimeType = FileUtils.getMimeType(mime) + val drawable = when (mimeType) { + FileUtils.MimeType.Image -> R.drawable.file_image + FileUtils.MimeType.Video -> R.drawable.file_video + FileUtils.MimeType.Audio -> R.drawable.file_audio + FileUtils.MimeType.Pdf -> R.drawable.file_pdf + FileUtils.MimeType.PlainText -> R.drawable.file_text + else -> R.drawable.file + } + drawable + } else if (content.isFileTransfer) { + R.drawable.download_simple + } else { + 0 + } + } + @WorkerThread private fun updateLastUpdatedTime() { val timestamp = chatRoom.lastUpdateTime From 8cce1223be1f1f6267b91c1cef89415ef2c925c7 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 29 May 2026 09:22:15 +0200 Subject: [PATCH 563/593] Restart keep alive service notification when it's dismissed --- .../notifications/NotificationsManager.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index 962b77da83..c0f6d6df7e 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -92,6 +92,7 @@ class NotificationsManager const val INTENT_TOGGLE_SPEAKER_CALL_NOTIF_ACTION = "org.linphone.TOGGLE_SPEAKER_CALL_ACTION" const val INTENT_REPLY_MESSAGE_NOTIF_ACTION = "org.linphone.REPLY_ACTION" const val INTENT_MARK_MESSAGE_AS_READ_NOTIF_ACTION = "org.linphone.MARK_AS_READ_ACTION" + const val INTENT_FOREGROUND_SERVICE_NOTIF_DISMISSED_ACTION = "org.linphone.INTENT_FOREGROUND_SERVICE_NOTIF_DISMISSED_ACTION" const val INTENT_ANSWER_CALL_NOTIF_CODE = 2 const val INTENT_HANGUP_CALL_NOTIF_CODE = 3 @@ -1904,6 +1905,7 @@ class NotificationsManager .setPriority(NotificationCompat.PRIORITY_LOW) .setShowWhen(false) .setContentIntent(pendingIntent) + .setDeleteIntent(getForegroundServiceDismissedIntent()) val notification = builder.build() Log.i( @@ -1924,6 +1926,23 @@ class NotificationsManager } } + @AnyThread + private fun getForegroundServiceDismissedIntent(): PendingIntent { + val foregroundServiceDismissedIntent = Intent( + context, + CoreKeepAliveThirdPartyAccountsService::class.java + ).apply { + action = INTENT_FOREGROUND_SERVICE_NOTIF_DISMISSED_ACTION + } + + return PendingIntent.getService( + context, + KEEP_ALIVE_FOR_THIRD_PARTY_ACCOUNTS_ID, + foregroundServiceDismissedIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } + @MainThread private fun stopKeepAliveServiceForeground() { val service = keepAliveService From 620fff3324e6fa00ce4100d22012e8e345be7847 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 29 May 2026 09:24:02 +0200 Subject: [PATCH 564/593] Removed click listener on helper text for delete native contacts --- app/src/main/res/layout/settings_developer_fragment.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/res/layout/settings_developer_fragment.xml b/app/src/main/res/layout/settings_developer_fragment.xml index bd52afe73b..492dc86011 100644 --- a/app/src/main/res/layout/settings_developer_fragment.xml +++ b/app/src/main/res/layout/settings_developer_fragment.xml @@ -335,7 +335,6 @@ Date: Mon, 1 Jun 2026 14:51:23 +0200 Subject: [PATCH 565/593] Updated CHANGELOG & bump version and name for 6.2.0 release --- CHANGELOG.md | 3 ++- app/build.gradle.kts | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d6682f5ba..f6d2fdbe1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Group changes to describe their impact on the project, as follows: Fixed for any bug fixes. Security to invite users to upgrade in case of vulnerabilities. -## [6.2.0] - Unreleased +## [6.2.0] - 2026-06-02 ### Added - Added the ability to edit/delete chat messages sent less than 24 hours ago. @@ -53,6 +53,7 @@ Group changes to describe their impact on the project, as follows: - Improved navigation within app when using a keyboard - Now loading media/documents contents in conversation by chunks (instead of all of them at once) - If in-call foreground service doesn't start, show an error notification and clicking on it will fix the issue (by bringing Linphone in foreground and re-starting the foreground service) +- Restart keep-alive service foreground notification if it's dismissed to ensure app will be kept alive in background - Simplified audio device name in settings - Reworked some settings (moved calls related ones from advanced settings to advanced calls settings) - Removed menu to access account profile, button is now directly available from drawer menu diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 71c9090635..b28a7fd2a6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -37,7 +37,7 @@ if (crashlyticsAvailable) { println("Crashlytics has been disabled because either google-services.json file wasn't found or local Linphone SDK build folder isn't configured") } -var gitVersion = "6.2.0-beta" +var gitVersion = "6.2.0" var gitBranch = "" try { val gitDescribe = ProcessBuilder() @@ -106,8 +106,8 @@ android { applicationId = packageName minSdk = 28 targetSdk = 37 - versionCode = 601012 // 6.01.012 - versionName = "6.2.0-beta" + versionCode = 602000 // 6.02.000 + versionName = "6.2.0" manifestPlaceholders["appAuthRedirectScheme"] = packageName From 5fa1a8aac8dd3535d36347662a185e81845122a0 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 9 Jun 2026 09:21:14 +0200 Subject: [PATCH 566/593] Prevent exception seen on Crashlytics --- .../org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt index 288c556e07..9939ee22a7 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt @@ -273,11 +273,11 @@ class FileViewModel viewModelScope.launch { withContext(Dispatchers.IO) { - val input = ParcelFileDescriptor.open( - File(filePath), - ParcelFileDescriptor.MODE_READ_ONLY - ) try { + val input = ParcelFileDescriptor.open( + File(filePath), + ParcelFileDescriptor.MODE_READ_ONLY + ) pdfRenderer = PdfRenderer(input) val count = pdfRenderer.pageCount Log.i("$TAG $count pages in file $filePath") From 84990f07d8d88fd7d901ddc6ef879d12cf2895f7 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 4 Jun 2026 10:06:42 +0200 Subject: [PATCH 567/593] Prevent active call screen from showing up when going back from outgoing call screen --- .../java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index d8c9bdfd7d..2d7acd3b1c 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -1094,7 +1094,7 @@ class CurrentCallViewModel Log.i("$TAG Conference [$subject] found, going to conference fragment") conferenceModel.configureFromCall(call) goToConferenceEvent.postValue(Event(true)) - } else { + } else if (LinphoneUtils.isCallActive(call.state)) { Log.i("$TAG No conference attached to this call, going to call fragment") conferenceModel.destroy() goToCallEvent.postValue(Event(true)) From afbca84b2c55a51a2908c1e2d423487cacb32b55 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 4 Jun 2026 10:10:10 +0200 Subject: [PATCH 568/593] Prevent search filter text from going behind the clear field icon in create chat room fragment --- app/src/main/res/layout/start_chat_fragment.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/layout/start_chat_fragment.xml b/app/src/main/res/layout/start_chat_fragment.xml index 616672d3eb..439ac2c36c 100644 --- a/app/src/main/res/layout/start_chat_fragment.xml +++ b/app/src/main/res/layout/start_chat_fragment.xml @@ -124,7 +124,7 @@ android:inputType="text|textUri|textNoSuggestions" android:paddingStart="15dp" android:paddingTop="10dp" - android:paddingEnd="15dp" + android:paddingEnd="45dp" android:paddingBottom="10dp" android:text="@={viewModel.searchFilter}" android:textSize="14sp" From d06900122f15ba9bee687b923aaa804b0bb2e7af Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 4 Jun 2026 13:56:35 +0200 Subject: [PATCH 569/593] Abort SSO process if server URL is null or empty --- .../ui/assistant/fragment/QrCodeScannerFragment.kt | 3 +-- .../fragment/ThirdPartySipAccountLoginFragment.kt | 2 +- app/src/main/java/org/linphone/ui/main/MainActivity.kt | 2 +- .../main/java/org/linphone/ui/sso/SingleSignOnActivity.kt | 7 ++++++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt index 824940f28c..41e79ba8c7 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/QrCodeScannerFragment.kt @@ -106,9 +106,8 @@ class QrCodeScannerFragment : GenericFragment() { it.consume { pair -> val serverUrl = pair.first val username = pair.second - Log.i( - "$TAG Navigating to Single Sign On Fragment with server URL [$serverUrl] and username [$username]" + "$TAG Bearer auth requested, navigating to Single Sign On Fragment with server URL [$serverUrl] and username [$username]" ) val intent = Intent(requireContext(), SingleSignOnActivity::class.java) intent.putExtra(SingleSignOnActivity.INTENT_EXTRA_USERNAME, username) diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountLoginFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountLoginFragment.kt index 440448832d..eda1fb8cd6 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountLoginFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountLoginFragment.kt @@ -139,7 +139,7 @@ class ThirdPartySipAccountLoginFragment : GenericFragment() { val username = pair.second Log.i( - "$TAG Navigating to Single Sign On Fragment with server URL [$serverUrl] and username [$username]" + "$TAG Bearer auth request, navigating to Single Sign On Fragment with server URL [$serverUrl] and username [$username]" ) val intent = Intent(requireContext(), SingleSignOnActivity::class.java) intent.putExtra(SingleSignOnActivity.INTENT_EXTRA_USERNAME, username) diff --git a/app/src/main/java/org/linphone/ui/main/MainActivity.kt b/app/src/main/java/org/linphone/ui/main/MainActivity.kt index d40d5b95b1..23f86abf84 100644 --- a/app/src/main/java/org/linphone/ui/main/MainActivity.kt +++ b/app/src/main/java/org/linphone/ui/main/MainActivity.kt @@ -295,7 +295,7 @@ class MainActivity : GenericActivity() { val username = pair.second Log.i( - "$TAG Navigating to Single Sign On Fragment with server URL [$serverUrl] and username [$username]" + "$TAG Bearer auth request, navigating to Single Sign On Fragment with server URL [$serverUrl] and username [$username]" ) val intent = Intent(this, SingleSignOnActivity::class.java) intent.putExtra(SingleSignOnActivity.INTENT_EXTRA_USERNAME, username) diff --git a/app/src/main/java/org/linphone/ui/sso/SingleSignOnActivity.kt b/app/src/main/java/org/linphone/ui/sso/SingleSignOnActivity.kt index 3bff188483..e9bdddb188 100644 --- a/app/src/main/java/org/linphone/ui/sso/SingleSignOnActivity.kt +++ b/app/src/main/java/org/linphone/ui/sso/SingleSignOnActivity.kt @@ -99,7 +99,12 @@ class SingleSignOnActivity : GenericActivity() { val username = intent.getStringExtra(INTENT_EXTRA_USERNAME).orEmpty() val serverUrl = intent.getStringExtra(INTENT_EXTRA_SERVER_URL).orEmpty() Log.i("$TAG Found server URL [$serverUrl] and username [$username] in args") - viewModel.setUp(serverUrl, username) + if (serverUrl.isEmpty()) { + Log.e("$TAG Server URL is empty, aborting authentication and going back") + finish() + } else { + viewModel.setUp(serverUrl, username) + } } @Deprecated("Deprecated in Java") From 55eebf659c20f9b5422163a2135f6d70caf3e50d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 9 Jun 2026 15:56:52 +0200 Subject: [PATCH 570/593] Prevent crashes due to lateinit property not being initialized (call recording player & chatroom) --- .../SendMessageInConversationViewModel.kt | 28 +++++++++++-------- .../RecordingMediaPlayerViewModel.kt | 8 ++++-- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt index 329cfd3bca..284bf8943b 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/SendMessageInConversationViewModel.kt @@ -616,19 +616,25 @@ class SendMessageInConversationViewModel isComputingParticipantsList.postValue(true) val participantsList = arrayListOf() - for (participant in chatRoom.participants) { - val model = ParticipantModel(participant.address, onClicked = { clicked -> - Log.i("$TAG Clicked on participant [${clicked.sipUri}]") - coreContext.postOnCoreThread { - val username = clicked.address.username - if (!username.isNullOrEmpty()) { - participantUsernameToAddEvent.postValue(Event(username.substring(participantsListFilter.length))) + if (::chatRoom.isInitialized) { + for (participant in chatRoom.participants) { + val model = ParticipantModel(participant.address, onClicked = { clicked -> + Log.i("$TAG Clicked on participant [${clicked.sipUri}]") + coreContext.postOnCoreThread { + val username = clicked.address.username + if (!username.isNullOrEmpty()) { + participantUsernameToAddEvent.postValue(Event(username.substring(participantsListFilter.length))) + } } + }) + + if ( + filter.isEmpty() || + participant.address.asStringUriOnly().contains(filter) || + model.avatarModel.contactName?.contains(filter) == true + ) { + participantsList.add(model) } - }) - - if (filter.isEmpty() || participant.address.asStringUriOnly().contains(filter) || model.avatarModel.contactName?.contains(filter) == true) { - participantsList.add(model) } } diff --git a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt index 36e7916c71..0033493601 100644 --- a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingMediaPlayerViewModel.kt @@ -113,8 +113,12 @@ class RecordingMediaPlayerViewModel fun setVideoRenderingSurface(textureView: TextureView) { val texture = textureView.surfaceTexture coreContext.postOnCoreThread { - Log.i("$TAG Setting window ID in player") - player.setWindowId(texture) + if (::player.isInitialized) { + Log.i("$TAG Setting window ID in player") + player.setWindowId(texture) + } else { + Log.e("$TAG Player hasn't been created yet, can't set the video texture") + } } } From 53e59d8f567b07741f087bc117d4a0f5392744a9 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 22 Jun 2026 10:16:28 +0200 Subject: [PATCH 571/593] Prevent DTMF sound to play indefinitely when swipping the numpad bottom sheet while pressing a digit --- app/src/main/java/org/linphone/utils/DataBindingUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 5cc171bcb7..4e3a9aece9 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -660,7 +660,7 @@ fun View.setTouchListener(listener: TouchListener) { setOnTouchListener { view, event -> return@setOnTouchListener when (event.action) { MotionEvent.ACTION_DOWN -> listener.onPressed(view) - MotionEvent.ACTION_UP -> listener.onReleased(view) + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> listener.onReleased(view) else -> false } } From a22d5d41615ce4fcdae8b2b0b9dcefae6f6970c6 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 22 Jun 2026 11:14:58 +0200 Subject: [PATCH 572/593] Improved margin in chat rooms list cell between icons & text --- app/src/main/res/layout/chat_list_cell.xml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/chat_list_cell.xml b/app/src/main/res/layout/chat_list_cell.xml index bb758c2ad4..726fdf1fd2 100644 --- a/app/src/main/res/layout/chat_list_cell.xml +++ b/app/src/main/res/layout/chat_list_cell.xml @@ -89,8 +89,7 @@ android:id="@+id/conversation_removal_in_progress" android:layout_width="@dimen/small_icon_size" android:layout_height="@dimen/small_icon_size" - android:layout_marginStart="@{model.lastMessageTextSender.length() > 0 && !model.isComposing ? @dimen/five : @dimen/zero}" - android:layout_marginEnd="5dp" + android:layout_marginEnd="3sp" android:layout_marginTop="3dp" android:src="@drawable/animated_in_progress" android:visibility="@{model.isBeingDeleted ? View.VISIBLE : View.GONE}" @@ -106,8 +105,7 @@ android:id="@+id/last_message_icon" android:layout_width="@dimen/small_icon_size" android:layout_height="@dimen/small_icon_size" - android:layout_marginStart="@{model.lastMessageTextSender.length() > 0 && !model.isComposing ? @dimen/five : @dimen/zero}" - android:layout_marginEnd="5dp" + android:layout_marginEnd="3sp" android:layout_marginTop="3dp" android:src="@{model.isComposing ? model.composingIcon : model.lastMessageContentIcon, default=@drawable/forward}" android:visibility="@{model.isBeingDeleted ? View.GONE : model.lastMessageContentIcon > 0 || model.isComposing ? View.VISIBLE : View.GONE}" From 0fea81975b69f6bb4a6b26587bdef98a606fa696 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 23 Jun 2026 07:16:07 +0000 Subject: [PATCH 573/593] Updated translations from Weblate --- app/src/main/res/values-lo/strings.xml | 448 +++++++++++++++++++++++++ 1 file changed, 448 insertions(+) create mode 100644 app/src/main/res/values-lo/strings.xml diff --git a/app/src/main/res/values-lo/strings.xml b/app/src/main/res/values-lo/strings.xml new file mode 100644 index 0000000000..0e1181e71b --- /dev/null +++ b/app/src/main/res/values-lo/strings.xml @@ -0,0 +1,448 @@ + + + ປະຕິເສດ + ຊື່ສະແດງ + ໂດເມນ + ຊື່ຜູ້ໃຊ້ + ID ການຢືນຢັນຕົວຕົນ (ຖ້າຕ່າງກັນ) + ລະຫັດຜ່ານ + ເບີໂທລະສັບ + ຫຼື + ຖັດໄປ + ເລີ່ມຕົ້ນ + ຄົ້ນຫາ + ຍອມຮັບ + ຍົກເລີກ + ດຳເນີນການຕໍ່ + ໂທ + ລຶບ + ID ອຸປະກອນ + ຂ້ອຍເຂົ້າໃຈແລ້ວ + ອຸປະກອນສຽງ + ເວົ້າບາງຢ່າງ… + ຖ່າຍຮູບ + ລວມທຸກສາຍເຂົ້າໃນການປະຊຸມບໍ? + ສ້າງການປະຊຸມ + ກວດສອບອຸປະກອນ + ລະຫັດຂອງເຈົ້າ: + ລະຫັດຂອງຄູ່ສົນທະນາ: + ຜູ້ເຂົ້າຮ່ວມ + ສ້າງການໂທກຸ່ມຫຼົ້ມເຫຼວ! + ກຳລັງເຂົ້າຮ່ວມ… + ພັກໄວ້ + ແບບຕາຕະລາງ + ຜູ້ເວົ້າ + ສຽງຢ່າງດຽວ + ໄຟລ໌ບັນທຶກສຽງ + ເບິ່ງຜູ້ຕິດຕໍ່ + ຕອບກັບ + ສົ່ງຕໍ່ + ຄັດລອກ + ດາວໂຫຼດ + ແບ່ງປັນ + ຜູ້ຕິດຕໍ່ + ຄຳແນະນຳ + ແກ້ໄຂຜູ້ຕິດຕໍ່ + ຕິດຕັ້ງ + ບໍ່ຕ້ອງສະແດງຂໍ້ຄວາມນີ້ອີກ + ບໍ່ + ແມ່ນ + ເອົາອອກ + ຢືນຢັນ + ເຂົ້າໃຈແລ້ວ + ໝາຍວ່າອ່ານແລ້ວ + ຕອບກັບ + ສາຍທີ່ບໍ່ໄດ້ຮັບ + ຍິນດີຕ້ອນຮັບ + ປອດໄພ + ໂອເພນຊອດ + ຄັດລອກທີ່ຢູ່ SIP ໃສ່ຄລິບບອດແລ້ວ + ນຳໃຊ້ການຕັ້ງຄ່າສຳເລັດແລ້ວ + ຂໍ້ກຳນົດທົ່ວໄປ ແລະ ນະໂຍບາຍຄວາມເປັນສ່ວນຕົວ + ຂໍ້ກຳນົດທົ່ວໄປ + ນະໂຍບາຍຄວາມເປັນສ່ວນຕົວ + ຢືນຢັນເບີໂທລະສັບ + ເຂົ້າສູ່ລະບົບ + ສະແກນລະຫັດ QR + ລະຫັດ QR ບໍ່ຖືກຕ້ອງ! + ໃຊ້ບັນຊີ SIP ຂອງພາກສ່ວນທີສາມ + ຍັງບໍ່ມີບັນຊີບໍ? + ລົງທະບຽນ + ເບີໂທບໍ່ຖືກຕ້ອງບໍ? + ສ້າງ + ສ້າງບັນຊີດ້ວຍອີເມວຂອງເຈົ້າທີ່: + ມີບັນຊີແລ້ວບໍ? + ໂປຣໂຕຄໍການຮັບສົ່ງ + ບໍ່ໄດ້ຮັບການແຈ້ງເຕືອນພຸຊພ້ອມລະຫັດຢືນຢັນພາຍໃນ 5 ວິນາທີ, ກະລຸນາລອງໃໝ່ພາຍຫຼັງ + ເກີດຂໍ້ຜິດພາດທີ່ບໍ່ຄາດຄິດ, ກະລຸນາລອງໃໝ່ພາຍຫຼັງ + ຊື່ຜູ້ໃຊ້ ຫຼື ລະຫັດຜ່ານບໍ່ຖືກຕ້ອງ + ອະນຸຍາດສິດການເຂົ້າເຖິງ + ຕົກລົງ + ໄວ້ເຮັດພາຍຫຼັງ + ຜູ້ຕິດຕໍ່ + ການໂທ + ການສົນທະນາ + ການປະຊຸມ + ຈັດການໂປຣໄຟລ໌ + ເຊື່ອມຕໍ່ແລ້ວ + ປິດໃຊ້ງານ + ກຳລັງເຊື່ອມຕໍ່… + ຜິດພາດ + ຍັງບໍ່ທັນມີການຕັ້ງຄ່າບັນຊີ + ເພີ່ມບັນຊີ + ຊ່ວຍເຫຼືອ + ຮຽນຮູ້ວິທີໃຊ້ງານທຸກຄຸນສົມບັດຂອງແອັບຯເທື່ອລະຂັ້ນຕອນ. + ນະໂຍບາຍຄວາມເປັນສ່ວນຕົວ + ເວີຊັນ + ກວດສອບການອັບເດດ + ຂັ້ນສູງ + ເວີຊັນຂອງເຈົ້າເປັນເວີຊັນຫຼ້າສຸດແລ້ວ + ເກີດຂໍ້ຜິດພາດຂະນະກວດສອບການອັບເດດ + ມີການອັບເດດໃໝ່ + ອອກຈາກແອັບຯ + ການແກ້ໄຂບັນຫາ + ສະແດງລັອກໃນ logcat + ລ້າງລັອກ (Logs) + ແບ່ງປັນລັອກ + ເວີຊັນແອັບຯ + ເວີຊັນ SDK + ID ໂຄງການ Firebase + ແບ່ງປັນລິ້ງລັອກດີບັກຜ່ານ… + ລ້າງລັອກດີບັກແລ້ວ + ອັບໂຫຼດລັອກດີບັກຫຼົ້ມເຫຼວ + ສະແດງການຕັ້ງຄ່າ + ການຕັ້ງຄ່າ + ຄວາມປອດໄພ + ເຂົ້າລະຫັດທຸກຢ່າງ + ຄຳເຕືອນ: ເມື່ອເປີດໃຊ້ແລ້ວ ຈະບໍ່ສາມາດປິດໄດ້! + ປ້ອງກັນການບັນທຶກໜ້າຈໍ ຫຼື ແຄັບໜ້າຈໍ + ການໂທ + ໃຊ້ຊອບແວຕັດສຽງສະທ້ອນ + ປ້ອງກັນສຽງສະທ້ອນບໍ່ໃຫ້ອີກຝ່າຍໄດ້ຍິນ ຫາກອຸປະກອນບໍ່ມີລະບົບຕັດສຽງສະທ້ອນໃນຕົວ + ປັບຕັ້ງລະບົບຕັດສຽງສະທ້ອນ + ກຳລັງດຳເນີນການ + ບໍ່ມີສຽງສະທ້ອນ + ຫຼົ້ມເຫຼວ + ການຄວບຄຸມອັດຕາຮັບສົ່ງແບບປັບຕົວ (Adaptive rate control) + ເປີດໃຊ້ Video FEC + ສັ່ນເວົ້າມີສາຍຮຽກເຂົ້າ + ເລີ່ມບັນທຶກການໂທໂດຍອັດຕະໂນມັດ + ປ່ຽນສຽງຮຽກເຂົ້າ + ການສົນທະນາ + ດາວໂຫຼດໄຟລ໌ອັດຕະໂນມັດ + ໝາຍວ່າອ່ານແລ້ວເມື່ອປິດການແຈ້ງເຕືອນຂໍ້ຄວາມ + ຜູ້ຕິດຕໍ່ + ເພີ່ມເຊີເວີ LDAP + ແກ້ໄຂເຊີເວີ LDAP + ເພີ່ມສະໝຸດລາຍຊື່ CardDAV + ແກ້ໄຂສະໝຸດລາຍຊື່ CardDAV + ຊື່ສະແດງ + URL ເຊີເວີ + ຊື່ຜູ້ໃຊ້ + ລະຫັດຜ່ານ + ການຊິງຂໍ້ມູນຜິດພາດ! + ເປີດໃຊ້ງານ + URL ເຊີເວີ (ຫ້າມປະຫວ່າງ) + Bind DN + ລະຫັດຜ່ານ + ໃຊ້ TLS + Search base (ຫ້າມປະຫວ່າງ) + ຕົວຕອງ + ຜົນລັບສູງສຸດ + ໝົດເວລາ (ເປັນວິນາທີ) + ໄລຍະຫ່າງລະຫວ່າງການຄົ້ນຫາ (ເປັນມິນລິວິນາທີ) + ຈຳນວນຕົວອັກສອນຂັ້ນຕ່ຳເພື່ອເລີ່ມຄົ້ນຫາ + Name attributes + SIP attributes + SIP domain + ເກີດຂໍ້ຜິດພາດ, ບໍ່ໄດ້ບັນທຶກເຊີເວີ LDAP! + ການປະຊຸມ + ສະແດງການປະຊຸມທີ່ຜ່ານມາ + ຮູບແບບເລີ່ມຕົ້ນ + ຜູ້ເວົ້າຫຼັກ + ແບບຕາຕະລາງ + ເຄືອຂ່າຍ + ໃຊ້ສະເພາະເຄືອຂ່າຍ Wi-Fi + ອະນຸຍາດ IPv6 + ປິດໃຊ້ງານ + ການຕັ້ງຄ່າຂັ້ນສູງ + ID ອຸປະກອນ + ໃຊ້ໄດ້ສະເພາະຕົວອັກສອນ ແລະ ຕົວເລກເທົ່ານັ້ນ + URL ເຊີເວີແບ່ງປັນໄຟລ໌ + ການຕັ້ງຄ່າສຳລັບນັກພັດທະນາ + URL ເຊີເວີແບ່ງປັນລັອກ + ການຕັ້ງຄ່າການໂທຂັ້ນສູງ + ການເຂົ້າລະຫັດສື່ + ບັງຄັບການເຂົ້າລະຫັດສື່ + ສ້າງການປະຊຸມ ແລະ ການໂທກຸ່ມແບບເຂົ້າລະຫັດ E2E + ເອີລີມີເດຍ (Early-media) + ຍອມຮັບ Early-media + ອະນຸຍາດ Early-media ຂາອອກ + URL ສຳລັບການຕັ້ງຄ່າຈາກທາງໄກ (Remote provisioning) + ດາວໂຫຼດ ແລະ ນຳໃຊ້ + ອຸປະກອນຮັບສຽງເລີ່ມຕົ້ນ + ອຸປະກອນອອກສຽງເລີ່ມຕົ້ນ + Audio codecs + Video codecs + ສະແດງການຕັ້ງຄ່າສຳລັບນັກພັດທະນາ + ຄລິກອີກ 2 ຄັ້ງເພື່ອເປີດການຕັ້ງຄ່າສຳລັບນັກພັດທະນາ + ຄລິກອີກ 1 ຄັ້ງເພື່ອເປີດການຕັ້ງຄ່າສຳລັບນັກພັດທະນາ + ເປີດໃຊ້ການຕັ້ງຄ່າສຳລັບນັກພັດທະນາແລ້ວ + ເປີດໃຊ້ການຕັ້ງຄ່າສຳລັບນັກພັດທະນາຢູ່ແລ້ວ + ສະແດງລະດັບສຽງ (VU meters) ຂະນະໂທ + ສະແດງສະຖິຕິການໂທຂັ້ນສູງ + ລາຍຊື່ໂດເມນທີ່ຮອງຮັບການແຈ້ງເຕືອນພຸຊ (ແຍກດ້ວຍຈຸດ) + ລຶບລາຍຊື່ຜູ້ຕິດຕໍ່ທີ່ນຳມາຈາກສະໝຸດໂທລະສັບ + ລາຍຊື່ຈະຖືກນຳເຂົ້າໃໝ່ເມື່ອເປີດແອັບຯຄັ້ງຖັດໄປ ເວັ້ນເສຍແຕ່ເຈົ້າຈະປິດສິດການເຂົ້າເຖິງຜູ້ຕິດຕໍ່ + ລຶບລາຍຊື່ຜູ້ຕິດຕໍ່ທີ່ນຳເຂົ້າແລ້ວ + ລຶບຂໍ້ມູນການຢືນຢັນຕົວຕົນທີ່ບໍ່ໄດ້ເຊື່ອມຕໍ່ກັບບັນຊີໃດໆ + ບໍ່ພົບຂໍ້ມູນການຢືນຢັນຕົວຕົນທີ່ຕົກຄ້າງ + ຈັດການບັນຊີ + ລາຍລະອຽດ + ອຸປະກອນ + ບໍ່ພົບອຸປະກອນ… + ເພີ່ມຮູບ + ແກ້ໄຂຮູບ + ເອົາຮູບອອກ + ບັນຊີນີ້ອອນລາຍຢູ່, ທຸກຄົນສາມາດໂທຫາເຈົ້າໄດ້. + ບັນຊີຖືກປິດໃຊ້ງານ, ເຈົ້າຈະບໍ່ໄດ້ຮັບການໂທ ຫຼື ຂໍ້ຄວາມໃດໆ. + ບັນຊີກຳລັງເຊື່ອມຕໍ່ຫາເຊີເວີ, ກະລຸນາຖ້າ… + ເຊື່ອມຕໍ່ບັນຊີຫຼົ້ມເຫຼວ, ກະລຸນາກວດສອບການຕັ້ງຄ່າ. + ລະຫັດປະເທດ + ການຕັ້ງຄ່າບັນຊີ + ອອກຈາກລະບົບ + ໂໝດເຮັດວຽກຮ່ວມກັນໄດ້ + ເອົາອອກ + ການເຊື່ອມຕໍ່ຫຼ້າສຸດ: + ອອກຈາກລະບົບຂອງເຈົ້າບໍ? + ຫາກເຈົ້າຕ້ອງການລຶບບັນຊີຖາວອນ, ກະລຸນາໄປທີ່: https://sip.linphone.org + SIP Proxy ຂາອອກ + ຫາກປ້ອນຂໍ້ມູນໃນຫ້ອງນີ້, Outbound Proxy ຈະຖືກເປີດໃຊ້ໂດຍອັດຕະໂນມັດ. ປະໄວ້ຫວ່າງຫາກຕ້ອງການປິດໃຊ້ງານ. + ການຕັ້ງຄ່າບັນຊີ + ອະນຸຍາດການແຈ້ງເຕືອນແບບພຸຊ + ການແຈ້ງເຕືອນແບບພຸຊ ບໍ່ພ້ອມໃຊ້ງານ! + ບັງຄັບການເຂົ້າລະຫັດ IM + URI ຜູ້ລົງທະບຽນ (Registrar) + URI ຂອງ SIP Proxy ຂາອອກ + ການຕັ້ງຄ່ານະໂຍບາຍ NAT + URL ເຊີເວີ STUN/TURN + ເປີດໃຊ້ ICE + ເປີດໃຊ້ TURN + ຊື່ຜູ້ໃຊ້ TURN + ລະຫັດຜ່ານ TURN + AVPF + ໝົດອາຍຸ (ເປັນວິນາທີ) + URI ສ້າງການປະຊຸມ + URI ສ້າງການປະຊຸມ ສຽງ/ວິດີໂອ + URL ເຊີເວີ CCMP + ໂໝດ Bundle + ໃຊ້ CPIM ໃນການສົນທະນາແບບ \"ພື້ນຖານ\" + URI ຂໍ້ຄວາມສຽງ + URI ເຊີເວີ MWI (Message Waiting Indicator) + ຈັດຮູບແບບເບີໂທລະສັບໂດຍໃຊ້ລະຫັດປະເທດ + ແທນທີ່ + ດ້ວຍ 00 ເວລາຈັດຮູບແບບເບີໂທລະສັບ + ອັບເດດລະຫັດຜ່ານ + ຈຳເປັນຕ້ອງມີການຢືນຢັນຕົວຕົນ + ລະຫັດຜ່ານ + ໂທໃໝ່ + ຄົ້ນຫາຜູ້ຕິດຕໍ່ ຫຼື ປະຫວັດການໂທ + ສ້າງການໂທກຸ່ມ + ຕັ້ງຫົວຂໍ້ການໂທກຸ່ມ + ຫົວຂໍ້ການໂທກຸ່ມ + ເຈົ້າຕ້ອງການລຶບປະຫວັດການໂທທັງໝົດແທ້ບໍ? + ຍັງບໍ່ມີຜູ້ຕິດຕໍ່ໃນຕອນນີ້… + ລາຍການທີ່ມັກ + ຜູ້ຕິດຕໍ່ທັງໝົດ + ເບິ່ງທັງໝົດ + ເບິ່ງຜູ້ຕິດຕໍ່ SIP + ເພີ່ມຜູ້ຕິດຕໍ່ໃໝ່ + ແກ້ໄຂຜູ້ຕິດຕໍ່ + ຊື່ + ນາມສະກຸນ + ບໍລິສັດ + ຕຳແໜ່ງງານ + ບໍ່ບັນທຶກການປ່ຽນແປງບໍ? + ການປ່ຽນແປງທັງໝົດຈະຖືກຍົກເລີກ + ຄວາມເຊື່ອຖື + ບໍ່ພົບອຸປະກອນ… + ຈຳນວນອຸປະກອນທີ່ເຊື່ອຖືໄດ້: + ການຈັດການອື່ນໆ + ແກ້ໄຂ + ເພີ່ມໃສ່ລາຍການທີ່ມັກ + ເອົາອອກຈາກລາຍການທີ່ມັກ + ແບ່ງປັນ + ລຶບ + ລຶບລາຍຊື່ຜູ້ຕິດຕໍ່ແລ້ວ + ເພີ່ມລະດັບຄວາມເຊື່ອຖື + ລະດັບຄວາມເຊື່ອຖື + ກວດສອບອຸປະກອນຜູ້ຕິດຕໍ່ທັງໝົດຂອງເຈົ້າ ເພື່ອໃຫ້ແນ່ໃຈວ່າການສື່ສານຈະປອດໄພ ແລະ ບໍ່ຖືກປ່ຽນແປງ.\nເມື່ອທຸກຢ່າງຖືກກວດສອບແລ້ວ, ເຈົ້າຈະບັນລຸລະດັບຄວາມເຊື່ອຖືສູງສຸດ. + ຂໍ້ຄວາມ + ກວດສອບ + ລາຍຊື່ຜູ້ຕິດຕໍ່ນີ້ຈະຖືກລຶບອອກຢ່າງຖາວອນ. + ເລືອກເບີໂທ ຫຼື ທີ່ຢູ່ SIP + ໂທ + ອຸປະກອນທີ່ບໍ່ມີຊື່ + ໝາຍວ່າອ່ານແລ້ວ + ປິດແຈ້ງເຕືອນ + ເປີດແຈ້ງເຕືອນ + ໂທ + ລຶບການສົນທະນາ + ອອກຈາກກຸ່ມ + ຕັ້ງຄ່າຂໍ້ຄວາມຊົ່ວຄາວ + ຂໍ້ຄວາມຊົ່ວຄາວ + ຂໍ້ຄວາມໃໝ່ຈະຖືກລຶບໂດຍອັດຕະໂນມັດເມື່ອທຸກຄົນອ່ານແລ້ວ.\nເລືອກໄລຍະເວລາ: + ປິດໃຊ້ງານ + 1 ນາທີ + 1 ຊົ່ວໂມງ + 1 ມື້ + 3 ມື້ + 1 ອາທິດ + ການສົນທະນາໃໝ່ + ຄົ້ນຫາຜູ້ຕິດຕໍ່ + ສ້າງການສົນທະນາກຸ່ມ + ເພີ່ມຜູ້ເຂົ້າຮ່ວມ + ຄົ້ນຫາ + ຂໍ້ມູນການສົນທະນາ + ຂໍ້ຄວາມຊົ່ວຄາວ + ສື່ + ເອກະສານ + ບໍ່ພົບສື່… + ບໍ່ພົບເອກະສານ… + ການສົນທະນາແບບເຂົ້າລະຫັດຕົ້ນທາງຫາປາຍທາງ + ຮັບປະກັນຄວາມເປັນສ່ວນຕົວ + ຂໍ້ຄວາມບໍ່ໄດ້ຖືກເຂົ້າລະຫັດແບບຕົ້ນທາງຫາປາຍທາງ, ລະວັງການແບ່ງປັນຂໍ້ມູນທີ່ສຳຄັນ! + ການສົນທະນານີ້ບໍ່ໄດ້ເຂົ້າລະຫັດ! + ຕັ້ງຫົວຂໍ້ການສົນທະນາ + ແກ້ໄຂຫົວຂໍ້ການສົນທະນາ + ຫົວຂໍ້ການສົນທະນາ + ລຶບຂໍ້ຄວາມແລ້ວ + ສ້າງການສົນທະນາຫຼົ້ມເຫຼວ! + ບໍ່ສາມາດສ້າງການສົນທະນາກັບຜູ້ເຂົ້າຮ່ວມທີ່ບໍ່ໄດ້ຢູ່ໃນໂດເມນດຽວກັນໄດ້ ເນື່ອງຈາກຂໍ້ຈຳກັດດ້ານຄວາມປອດໄພ! + ເປີດການໃຊ້ຂໍ້ຄວາມຊົ່ວຄາວແລ້ວ + ປິດການໃຊ້ຂໍ້ຄວາມຊົ່ວຄາວແລ້ວ + ເຈົ້າໄດ້ອອກຈາກກຸ່ມແລ້ວ + ບໍ່ພົບຜົນການຄົ້ນຫາ + ເປີດແກເລີຣີ + ເລືອກໄຟລ໌ + ບໍ່ສາມາດເປີດໄຟລ໌ໄດ້! + ກຳລັງແກ້ໄຂຂໍ້ຄວາມ + ແກ້ໄຂແລ້ວ + ລຶບສຳລັບຂ້ອຍ + ລຶບໃຫ້ທຸກຄົນ + ຜູ້ເຂົ້າຮ່ວມ + ບໍ່ພົບຜູ້ເຂົ້າຮ່ວມ + ເພີ່ມຜູ້ເຂົ້າຮ່ວມ + ຜູ້ດູແລ + ລຶບປະຫວັດ + ເອົາອອກຈາກກຸ່ມ + ຕັ້ງເປັນຜູ້ດູແລ + ເອົາສິດຜູ້ດູແລອອກ + ເບິ່ງໂປຣໄຟລ໌ຜູ້ຕິດຕໍ່ + ເພີ່ມໃສ່ລາຍຊື່ຜູ້ຕິດຕໍ່ + ເຈົ້າຕ້ອງການລຶບຂໍ້ຄວາມທັງໝົດແທ້ບໍ? + ລຶບປະຫວັດສຳເລັດແລ້ວ + ເລີ່ມການໂທກຸ່ມບໍ? + ຜູ້ເຂົ້າຮ່ວມທັງໝົດຈະໄດ້ຮັບສາຍຮຽກເຂົ້າ. + ເຈົ້າໄດ້ເຂົ້າຮ່ວມກຸ່ມແລ້ວ + ເຈົ້າໄດ້ອອກຈາກກຸ່ມແລ້ວ + ເປີດການໃຊ້ຂໍ້ຄວາມຊົ່ວຄາວແລ້ວ + ປິດການໃຊ້ຂໍ້ຄວາມຊົ່ວຄາວແລ້ວ + ສື່ ແລະ ເອກະສານ + ສື່ທີ່ແບ່ງປັນ + ເອກະສານທີ່ແບ່ງປັນ + ສົ່ງຕໍ່ຂໍ້ຄວາມໄປຫາ… + ສົ່ງຕໍ່ຂໍ້ຄວາມແລ້ວ + ຍົກເລີກການສົ່ງຕໍ່ຂໍ້ຄວາມແລ້ວ + ຄລິກເພື່ອເອົາອອກ + ສົ່ງຕໍ່ແລ້ວ + ບໍ່ມີການນັດໝາຍການປະຊຸມໃນມື້ນີ້ + ການປະຊຸມໃໝ່ + ການປະຊຸມ + ເພີ່ມຫົວຂໍ້… + ເລືອກວັນທີເລີ່ມຕົ້ນ + ເລືອກເວລາເລີ່ມຕົ້ນ + ເລືອກເວລາສິ້ນສຸດ + ເຂດເວລາ + ເພີ່ມຄຳອະທິບາຍ + ເພີ່ມຜູ້ເຂົ້າຮ່ວມ + ສົ່ງຄຳເຊີນໃຫ້ຜູ້ເຂົ້າຮ່ວມ + ເຂົ້າຮ່ວມການປະຊຸມດຽວນີ້ + ຜູ້ຈັດການປະຊຸມ + ສ້າງກິດຈະກຳໃນປະຕິທິນ + ລຶບການປະຊຸມແລ້ວ + ຄຳອະທິບາຍ + ແກ້ໄຂການປະຊຸມ + ລຶບການປະຊຸມ + ສ້າງການປະຊຸມແລ້ວ + ອັບເດດການປະຊຸມແລ້ວ + ການປະຊຸມຖືກຍົກເລີກແລ້ວ + ນັດໝາຍການປະຊຸມຫຼົ້ມເຫຼວ! + ສົ່ງຄຳເຊີນການປະຊຸມຫຼົ້ມເຫຼວ! + ສົ່ງຄຳເຊີນໃຫ້ຜູ້ເຂົ້າຮ່ວມບາງຄົນຫຼົ້ມເຫຼວ! + ເຂົ້າຮ່ວມ + ຍົກເລີກ + ກຳລັງເຊື່ອມຕໍ່ + ສາຍໂທອອກ + ສາຍຮຽກເຂົ້າ + ສາຍທີ່ກຳລັງລົມຢູ່ + ບໍ່ມີສາຍອື່ນ + ຢືນຢັນການໂອນສາຍ + ໂອນສາຍ + ໂທໃໝ່ + ລາຍການການໂທ + ແປ້ນໂທ + ຂໍ້ຄວາມ + ພັກສາຍ + ລົມຕໍ່ + ບັນທຶກ + ວາງສາຍ + ຮູບແບບການສະແດງ + ກຳລັງລົມ + ພັກໄວ້ + ຖືກພັກສາຍໂດຍອີກຝ່າຍ + ກຳລັງກັບເຂົ້າສູ່ການລົມ… + ກຳລັງຖ້າການເຂົ້າລະຫັດ… + ເຂົ້າລະຫັດແບບຕົ້ນທາງຫາປາຍທາງດ້ວຍ ZRTP + ເຂົ້າລະຫັດແບບຈຸດຕໍ່ຈຸດດ້ວຍ ZRTP + ເຂົ້າລະຫັດແບບຕົ້ນທາງຫາປາຍທາງ (E2E) + ກວດສອບ ZRTP SAS ຄືນໃໝ່ + ຈຳເປັນຕ້ອງມີການກວດສອບ + ເຂົ້າລະຫັດແບບຈຸດຕໍ່ຈຸດດ້ວຍ SRTP + ການໂທບໍ່ໄດ້ເຂົ້າລະຫັດ + ລາຍການການໂທ + ກຳລັງບັນທຶກການໂທ + ບັນທຶກການໂທຖືກເກັບໄວ້ແລ້ວ + ບໍ່ມີອັນໃດກົງກັນ + ແຈ້ງເຕືອນຄວາມປອດໄພ + ລອງໃໝ່ + ຄວາມລັບຂອງການໂທນີ້ອາດຈະຖືກແຊກແຊງ! + ລຳໂພງຫູຟັງ + ຜູ້ເວົ້າ + ຫູຟັງ + ສຽງ + ວິດີໂອ + ການເຂົ້າລະຫັດສື່ + ລຶບປະຫວັດແລ້ວ + ກຳລັງໂອນສາຍ + ໂອນສາຍສຳເລັດແລ້ວ + ການໂອນສາຍຫຼົ້ມເຫຼວ! + ແບ່ງປັນຄຳເຊີນ + ກຳລັງຖ້າຜູ້ເຂົ້າຮ່ວມຄົນອື່ນ… + ແບ່ງປັນໜ້າຈໍ + ຍັງບໍ່ມີໄຟລ໌ບັນທຶກສຽງໃນຕອນນີ້… + ເພີ່ມໃສ່ລາຍຊື່ຜູ້ຕິດຕໍ່ + ຄັດລອກເບີໂທລະສັບ + ລຶບປະຫວັດ + ລຶບ + ເຊີນ + ສົ່ງຄືນໃໝ່ + ສະຖານະການສົ່ງ + ແກ້ໄຂ + ບໍ່ພົບຜົນລັບ… + ການເຊື່ອມຕໍ່ບັນຊີຜິດພາດ + ບັນຊີທີ່ເລືອກຖືກປິດໃຊ້ງານໃນຂະນະນີ້ + ເຈົ້າບໍ່ໄດ້ເຊື່ອມຕໍ່ອິນເຕີເນັດ + ກຳລັງດຳເນີນການ, ກະລຸນາຖ້າ + ການສົນທະນາ + ເພີ່ມຜູ້ເຂົ້າຮ່ວມ + From b65f3aa0ed8a35c21213316d0a03af243b0951bd Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 23 Jun 2026 14:38:31 +0200 Subject: [PATCH 574/593] Try to automatically use headphones / headset if possible --- .../main/java/org/linphone/core/CoreContext.kt | 18 +++++++++++++----- .../main/java/org/linphone/utils/AudioUtils.kt | 18 ++++++++++++++++-- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 35bca40585..91cd288e97 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -144,6 +144,7 @@ class CoreContext if (!addedDevices.isNullOrEmpty()) { Log.i("$TAG [${addedDevices.size}] new device(s) have been added:") var atLeastOneNewDeviceIsBluetooth = false + var atLeastOneNewDeviceIsHeadset = false for (device in addedDevices) { Log.i( "$TAG Added device [${device.productName}] with ID [${device.id}] and type [${device.type}]" @@ -153,6 +154,10 @@ class CoreContext AudioDeviceInfo.TYPE_BLUETOOTH_SCO, AudioDeviceInfo.TYPE_BLE_HEADSET, AudioDeviceInfo.TYPE_BLE_SPEAKER, AudioDeviceInfo.TYPE_HEARING_AID, AudioDeviceInfo.TYPE_BLE_HEARING_AID -> { atLeastOneNewDeviceIsBluetooth = true } + + AudioDeviceInfo.TYPE_USB_HEADSET, AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET -> { + atLeastOneNewDeviceIsHeadset = true + } else -> {} } } @@ -164,7 +169,10 @@ class CoreContext if (atLeastOneNewDeviceIsBluetooth && core.callsNb > 0 && corePreferences.routeAudioToBluetoothWhenPossible) { Log.i("$TAG It seems a bluetooth device is now available, trying to route audio to it") - AudioUtils.routeAudioToEitherBluetoothOrHearingAid() + AudioUtils.routeAudioBluetoothOrHearingAid() + } else if (atLeastOneNewDeviceIsHeadset && core.callsNb > 0) { + Log.i("$TAG It seems a headset or headphones device is now available, trying to route audio to it") + AudioUtils.routeAudioToHeadset() } }, 500) } @@ -358,8 +366,8 @@ class CoreContext } Call.State.OutgoingRinging, Call.State.OutgoingEarlyMedia -> { if (corePreferences.routeAudioToBluetoothWhenPossible) { - Log.i("$TAG Trying to route audio to either bluetooth or hearing aid if available") - AudioUtils.routeAudioToEitherBluetoothOrHearingAid(call) + Log.i("$TAG Trying to route audio to either bluetooth, hearing aid, headphones or headset if available") + AudioUtils.routeAudioToAnyConnectedAudioDeviceOtherThanEarpieceAndSpeaker(call) } } Call.State.Connected -> { @@ -367,8 +375,8 @@ class CoreContext showCallActivity() } if (corePreferences.routeAudioToBluetoothWhenPossible) { - Log.i("$TAG Call is connected, trying to route audio to either bluetooth or hearing aid if available") - AudioUtils.routeAudioToEitherBluetoothOrHearingAid(call) + Log.i("$TAG Call is connected, trying to route audio to either bluetooth, hearing aid, headphones or headset if available") + AudioUtils.routeAudioToAnyConnectedAudioDeviceOtherThanEarpieceAndSpeaker(call) } } Call.State.StreamsRunning -> { diff --git a/app/src/main/java/org/linphone/utils/AudioUtils.kt b/app/src/main/java/org/linphone/utils/AudioUtils.kt index 6ffa2e4d02..7ddee7ff54 100644 --- a/app/src/main/java/org/linphone/utils/AudioUtils.kt +++ b/app/src/main/java/org/linphone/utils/AudioUtils.kt @@ -56,8 +56,22 @@ class AudioUtils { } @WorkerThread - fun routeAudioToEitherBluetoothOrHearingAid(call: Call? = null) { - routeAudioTo(call, arrayListOf(AudioDevice.Type.Bluetooth, AudioDevice.Type.HearingAid)) + fun routeAudioBluetoothOrHearingAid(call: Call? = null) { + routeAudioTo( + call, + arrayListOf(AudioDevice.Type.Bluetooth, AudioDevice.Type.HearingAid) + ) + } + + @WorkerThread + fun routeAudioToAnyConnectedAudioDeviceOtherThanEarpieceAndSpeaker(call: Call? = null) { + routeAudioTo( + call, + arrayListOf( + AudioDevice.Type.Bluetooth, AudioDevice.Type.HearingAid, + AudioDevice.Type.Headphones, AudioDevice.Type.Headset + ) + ) } @WorkerThread From b7a5826f7d14b12ebd1448c0bc220c02646930a9 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 12 Jun 2026 11:03:33 +0200 Subject: [PATCH 575/593] Added proximity sensor listener --- .../java/org/linphone/core/CoreContext.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 91cd288e97..46013d2a91 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -24,7 +24,12 @@ import android.app.Application import android.app.PendingIntent import android.content.Context import android.content.Context.POWER_SERVICE +import android.content.Context.SENSOR_SERVICE import android.content.Intent +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager import android.media.AudioDeviceCallback import android.media.AudioDeviceInfo import android.media.AudioManager @@ -599,6 +604,24 @@ class CoreContext } } + private val proximitySensorListener = object : SensorEventListener { + override fun onAccuracyChanged( + sensor: Sensor?, + accuracy: Int + ) { + } + + override fun onSensorChanged(event: SensorEvent?) { + if (event?.sensor?.type == Sensor.TYPE_PROXIMITY) { + if (event.values[0] == 0f) { + Log.i("$TAG Proximity sensor triggered, screen will turn off") + } else { + Log.i("$TAG Proximity sensor released, screen will turn back on") + } + } + } + } + init { (context as Application).registerActivityLifecycleCallbacks(activityMonitor) } @@ -773,12 +796,21 @@ class CoreContext PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "${context.packageName};proximity_sensor" ) + val sensorManager = context.getSystemService(SENSOR_SERVICE) as SensorManager + val proximity = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY) + val added = sensorManager.registerListener(proximitySensorListener, proximity, SensorManager.SENSOR_DELAY_NORMAL) + if (!added) { + Log.e("$TAG Failed to add proximity sensor listener!") + } } } @WorkerThread private fun onCoreStopped() { Log.w("$TAG Core is being shut down, notifying managers so they can remove their listeners and do some cleanup if needed") + val sensorManager = context.getSystemService(SENSOR_SERVICE) as SensorManager + sensorManager.unregisterListener(proximitySensorListener) + contactsManager.onCoreStopped(core) telecomManager.onCoreStopped(core) notificationsManager.onCoreStopped(core) From 4da02e4979642b104ed54cd353eb9b8ff0145d75 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 23 Jun 2026 15:27:11 +0200 Subject: [PATCH 576/593] Updated CHANGELOG & bumped version code & name, added Laotian translation from Weblate --- CHANGELOG.md | 14 ++++++++++++++ app/build.gradle.kts | 6 +++--- app/src/main/res/xml/locales_config.xml | 1 + 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6d2fdbe1e..b6f29b4818 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,20 @@ Group changes to describe their impact on the project, as follows: Fixed for any bug fixes. Security to invite users to upgrade in case of vulnerabilities. +## [6.2.1] - 2026-06-23 + +### Added +- Added proximity sensor listener to be able to log events +- Laotian translation from Weblate + +### Changed +- Abort single sign-on process if server URL is null or empty +- Automatically route audio to wired headphones/headset if such device is available + +### Fixed +- Active call screen showing up after going back from outgoing call screen +- Prevent DTMF to be played indefinitely when swiping the numpad bottom sheet away while pressing a digit + ## [6.2.0] - 2026-06-02 ### Added diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b28a7fd2a6..e931c85aca 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -37,7 +37,7 @@ if (crashlyticsAvailable) { println("Crashlytics has been disabled because either google-services.json file wasn't found or local Linphone SDK build folder isn't configured") } -var gitVersion = "6.2.0" +var gitVersion = "6.2.1" var gitBranch = "" try { val gitDescribe = ProcessBuilder() @@ -106,8 +106,8 @@ android { applicationId = packageName minSdk = 28 targetSdk = 37 - versionCode = 602000 // 6.02.000 - versionName = "6.2.0" + versionCode = 602001 // 6.02.001 + versionName = "6.2.1" manifestPlaceholders["appAuthRedirectScheme"] = packageName diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index 3cbb8fac2a..273961c42f 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -6,6 +6,7 @@ + From f94addb9d752c2f1155f01185eb9c2d31ed87a4f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 24 Jun 2026 10:26:49 +0200 Subject: [PATCH 577/593] Fixed part of drawer menu hidden behind nav bar when phone is in landscape mode --- app/src/main/java/org/linphone/ui/main/MainActivity.kt | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/MainActivity.kt b/app/src/main/java/org/linphone/ui/main/MainActivity.kt index 23f86abf84..90c976603b 100644 --- a/app/src/main/java/org/linphone/ui/main/MainActivity.kt +++ b/app/src/main/java/org/linphone/ui/main/MainActivity.kt @@ -29,7 +29,6 @@ import android.net.Uri import android.os.Bundle import android.os.Parcelable import android.view.Gravity -import android.view.ViewGroup import android.view.ViewTreeObserver import android.view.WindowManager import androidx.activity.SystemBarStyle @@ -170,12 +169,7 @@ class MainActivity : GenericActivity() { ViewCompat.setOnApplyWindowInsetsListener(binding.drawerMenuContent) { v, windowInsets -> val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) - val mlp = v.layoutParams as ViewGroup.MarginLayoutParams - mlp.leftMargin = insets.left - mlp.topMargin = insets.top - mlp.rightMargin = insets.right - mlp.bottomMargin = insets.bottom - v.layoutParams = mlp + v.updatePadding(insets.left, insets.top, insets.right, insets.bottom) WindowInsetsCompat.CONSUMED } From 879da315eadc5e0a17fc10a0c1f2fd75f3e3b526 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 24 Jun 2026 10:36:50 +0200 Subject: [PATCH 578/593] Updated vu-meter max value --- .../java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index 2d7acd3b1c..eb2786c7be 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -75,7 +75,7 @@ class CurrentCallViewModel companion object { private const val TAG = "[Current Call ViewModel]" private const val VU_METER_MIN = -20f - private const val VU_METER_MAX = 4 + private const val VU_METER_MAX = 0 } val contact = MutableLiveData() From 8c74e923153b1c61b4b4c0fa047ce2a0086087fb Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 24 Jun 2026 19:28:11 +0200 Subject: [PATCH 579/593] Improved keep app alive service foreground notification text --- .../java/org/linphone/notifications/NotificationsManager.kt | 3 ++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index c0f6d6df7e..a2020912c4 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -1897,7 +1897,8 @@ class NotificationsManager val builder = NotificationCompat.Builder(context, channelId) .setSmallIcon(R.drawable.linphone_notification) - .setContentText(AppUtils.getString(R.string.notification_keep_app_alive_message)) + .setContentText(AppUtils.getString(R.string.notification_keep_app_alive_description)) + .setSubText(AppUtils.getString(R.string.notification_keep_app_alive_message)) .setAutoCancel(false) .setOngoing(true) .setCategory(NotificationCompat.CATEGORY_SERVICE) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b081a382f6..a2c8d06c89 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -69,6 +69,7 @@ %s fichiers en cours de réception Cliquez pour ouvrir + Cette notification maintient l\'app en vie en arrière plan Activer haut-parleur Désactiver haut-parleur Compte %s en erreur ! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8e8c9a11d1..cc0491d412 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -114,6 +114,7 @@ %s, %s Click to open + This notification keeps app alive while in background Turn on speaker Turn off speaker Account %s registration failed! From 663418f5bdd408e0ee6eb0c1afe3716c61486d8e Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 25 Jun 2026 11:58:55 +0200 Subject: [PATCH 580/593] Only resume media player after seeking if it was playing before --- .../linphone/ui/fileviewer/fragment/MediaViewerFragment.kt | 5 ++++- .../org/linphone/ui/fileviewer/viewmodel/MediaViewModel.kt | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/fileviewer/fragment/MediaViewerFragment.kt b/app/src/main/java/org/linphone/ui/fileviewer/fragment/MediaViewerFragment.kt index 7d278161f1..008fee9cab 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/fragment/MediaViewerFragment.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/fragment/MediaViewerFragment.kt @@ -46,18 +46,21 @@ class MediaViewerFragment : GenericMainFragment() { private lateinit var viewModel: MediaViewModel + private var wasVideoPlayingBeforeTrackingTouch = false + private val seekBarListener = object : SeekBar.OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { } override fun onStartTrackingTouch(seekBar: SeekBar) { + wasVideoPlayingBeforeTrackingTouch = viewModel.isMediaPlaying.value == true viewModel.pause() } override fun onStopTrackingTouch(seekBar: SeekBar) { val newPosition = seekBar.progress - viewModel.seekTo(newPosition) + viewModel.seekTo(newPosition, wasVideoPlayingBeforeTrackingTouch) } } diff --git a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/MediaViewModel.kt b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/MediaViewModel.kt index 5ab23b9888..08607f9128 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/MediaViewModel.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/MediaViewModel.kt @@ -162,10 +162,12 @@ class MediaViewModel } @UiThread - fun seekTo(position: Int) { + fun seekTo(position: Int, resumePlay: Boolean) { if (::mediaPlayer.isInitialized) { mediaPlayer.seekTo(position) - play() + if (resumePlay) { + play() + } } } From 00478b7c721f31cd13c41f82a5988cc55567b170 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 25 Jun 2026 16:21:19 +0200 Subject: [PATCH 581/593] Bumped version code & updated CHANGELOG for 6.2.2 --- CHANGELOG.md | 9 +++++++++ app/build.gradle.kts | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6f29b4818..423c220bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,15 @@ Group changes to describe their impact on the project, as follows: Fixed for any bug fixes. Security to invite users to upgrade in case of vulnerabilities. +## [6.2.2] - 2026-06-25 + +### Changed +- Improved keep app alive foreground service notification content text +- Only resume media player after seeking if it was playing before + +### Fixed +- Prevent drawer menu to be drawn behind the navigation bar in landscape + ## [6.2.1] - 2026-06-23 ### Added diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e931c85aca..494b606b6e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -37,7 +37,7 @@ if (crashlyticsAvailable) { println("Crashlytics has been disabled because either google-services.json file wasn't found or local Linphone SDK build folder isn't configured") } -var gitVersion = "6.2.1" +var gitVersion = "6.2.2" var gitBranch = "" try { val gitDescribe = ProcessBuilder() @@ -106,8 +106,8 @@ android { applicationId = packageName minSdk = 28 targetSdk = 37 - versionCode = 602001 // 6.02.001 - versionName = "6.2.1" + versionCode = 602002 // 6.02.002 + versionName = "6.2.2" manifestPlaceholders["appAuthRedirectScheme"] = packageName From 8a7fbd926840a3cfd0905c279e42726b68b074a7 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 1 Jul 2026 15:28:23 +0200 Subject: [PATCH 582/593] Added setting to disable use of proximity sensor in audio calls --- .../java/org/linphone/core/CoreContext.kt | 5 +++ .../java/org/linphone/core/CorePreferences.kt | 7 ++++ .../settings/viewmodel/SettingsViewModel.kt | 12 +++++++ .../settings_advanced_calls_fragment.xml | 32 ++++++++++++++++++- app/src/main/res/values-fr/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 6 files changed, 57 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 46013d2a91..f98be32a63 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -1344,6 +1344,11 @@ class CoreContext @UiThread fun enableProximitySensor(enable: Boolean) { + if (enable && !corePreferences.useProximitySensor) { + Log.w("$TAG App tried to enable proximity sensor but it's been disabled in settings, doing nothing") + return + } + if (::proximityWakeLock.isInitialized) { if (enable && !proximityWakeLock.isHeld) { Log.i("$TAG Acquiring proximity sensor wake lock for 2 hours") diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 8d90a06961..28c22a6f87 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -204,6 +204,13 @@ class CorePreferences config.setBool("ui", "show_advanced_call_stats", value) } + @get:AnyThread @set:WorkerThread + var useProximitySensor: Boolean + get() = config.getBool("ui", "use_proximity_sensor", true) + set(value) { + config.setBool("ui", "use_proximity_sensor", value) + } + // Conversation related @get:AnyThread @set:WorkerThread diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index 5e997ee373..9afcf6c3de 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -231,6 +231,7 @@ class SettingsViewModel val mediaEncryptionMandatory = MutableLiveData() val rfc2833Dtmf = MutableLiveData() val sipInfoDtmf = MutableLiveData() + val useProximitySensor = MutableLiveData() val acceptEarlyMedia = MutableLiveData() val ringDuringEarlyMedia = MutableLiveData() val allowOutgoingEarlyMedia = MutableLiveData() @@ -384,6 +385,7 @@ class SettingsViewModel rfc2833Dtmf.postValue(core.useRfc2833ForDtmf) sipInfoDtmf.postValue(core.useInfoForDtmf) + useProximitySensor.postValue(corePreferences.useProximitySensor) acceptEarlyMedia.postValue(corePreferences.acceptEarlyMedia) ringDuringEarlyMedia.postValue(core.ringDuringIncomingEarlyMedia) @@ -944,6 +946,16 @@ class SettingsViewModel } } + @UiThread + fun toggleUseProximitySensor() { + val newValue = useProximitySensor.value == false + + coreContext.postOnCoreThread { + corePreferences.useProximitySensor = newValue + useProximitySensor.postValue(newValue) + } + } + @UiThread fun toggleEarlyMediaExpand() { expandEarlyMedia.value = expandEarlyMedia.value == false diff --git a/app/src/main/res/layout/settings_advanced_calls_fragment.xml b/app/src/main/res/layout/settings_advanced_calls_fragment.xml index 57e372050f..f0f7a59073 100644 --- a/app/src/main/res/layout/settings_advanced_calls_fragment.xml +++ b/app/src/main/res/layout/settings_advanced_calls_fragment.xml @@ -278,7 +278,37 @@ android:layout_marginEnd="16dp" android:checked="@{viewModel.sipInfoDtmf}" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@id/rfc_2833_dtmf_switch" + app:layout_constraintTop_toBottomOf="@id/rfc_2833_dtmf_switch"/> + + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index a2c8d06c89..4b47eb1d11 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -219,6 +219,7 @@ Le sélectionneur de sonnerie n\'est pas disponible ! Utiliser la RFC 2833 pour les DTMFs Utiliser SIP INFO pour les DTMFs + Utiliser le détecteur de proximité pour éteindre l\'écran quand proche de l\'oreille Conversations Télécharger automatiquement les fichiers Rendre visible dans la galerie les médias téléchargés diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cc0491d412..b789104422 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -271,6 +271,7 @@ Ringtone picker isn\'t available! Use RFC 2833 for DTMFs Use SIP INFO for DTMFs + Use proximity sensor to turn OFF the screen when near the ear Conversations Auto-download files Make downloaded media public From 558f34a28d90fbd6e5a656d9eaff732456c206e0 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 2 Jul 2026 09:49:41 +0200 Subject: [PATCH 583/593] Hide paused label in conf while in fullscreen, leave fullscreen mode when call is being paused by remote --- .../org/linphone/ui/call/fragment/ActiveCallFragment.kt | 9 +++++++++ .../main/res/layout/call_active_conference_fragment.xml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt index 9c8e10ba20..df65452c7b 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/ActiveCallFragment.kt @@ -234,6 +234,15 @@ class ActiveCallFragment : GenericCallFragment() { callMediaEncryptionStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN } + callViewModel.isPausedByRemote.observe(viewLifecycleOwner) { paused -> + if (paused) { + if (callViewModel.fullScreenMode.value == true) { + Log.i("$TAG Call is paused by remote, leaving full screen mode") + callViewModel.fullScreenMode.postValue(false) + } + } + } + callViewModel.showZrtpSasDialogEvent.observe(viewLifecycleOwner) { it.consume { pair -> callMediaEncryptionStatsBottomSheetBehavior.state = BottomSheetBehavior.STATE_HIDDEN diff --git a/app/src/main/res/layout/call_active_conference_fragment.xml b/app/src/main/res/layout/call_active_conference_fragment.xml index 4665f71fd5..8547881cc6 100644 --- a/app/src/main/res/layout/call_active_conference_fragment.xml +++ b/app/src/main/res/layout/call_active_conference_fragment.xml @@ -130,7 +130,7 @@ android:layout_height="wrap_content" android:layout_marginStart="5dp" android:text="@string/call_state_paused" - android:visibility="@{conferenceViewModel.isPaused ? View.VISIBLE : View.GONE, default=gone}" + android:visibility="@{!viewModel.fullScreenMode && !viewModel.pipMode && conferenceViewModel.isPaused ? View.VISIBLE : View.GONE, default=gone}" app:layout_constraintHorizontal_bias="0" app:layout_constraintStart_toEndOf="@id/chronometer" app:layout_constraintEnd_toStartOf="@id/switch_camera" From 40929b773f87f7d0f7ee5015dcc18c2fafc95f9c Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Thu, 2 Jul 2026 14:43:22 +0200 Subject: [PATCH 584/593] Fixed registrar & outbound SIP proxy transport URIs that could not be changed after account was configured --- .../ui/main/settings/viewmodel/AccountSettingsViewModel.kt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt index 89ebbbf5cf..7bba055711 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountSettingsViewModel.kt @@ -59,8 +59,6 @@ class AccountSettingsViewModel val availableTransports = arrayListOf() - val selectedTransport = MutableLiveData() - val sipProxyServer = MutableLiveData() val outboundProxyServer = MutableLiveData() @@ -152,9 +150,6 @@ class AccountSettingsViewModel imEncryptionMandatory.postValue(params.instantMessagingEncryptionMandatory) - val transportType = params.serverAddress?.transport ?: TransportType.Tls - selectedTransport.postValue(transportType) - sipProxyServer.postValue(params.serverAddress?.asStringUriOnly()) if (params.routesAddresses.isNotEmpty()) { outboundProxyServer.postValue(params.routesAddresses.first().asStringUriOnly()) @@ -228,7 +223,6 @@ class AccountSettingsViewModel Log.i("$TAG Proxy server set to [$server]") val serverAddress = core.interpretUrl(server, false) if (serverAddress != null) { - serverAddress.transport = selectedTransport.value newParams.serverAddress = serverAddress } else { Log.e("$TAG Failed to parse proxy server!") @@ -239,7 +233,6 @@ class AccountSettingsViewModel Log.i("$TAG Outbound proxy server set to [$outboundProxy]") val outboundProxyAddress = core.interpretUrl(outboundProxy, false) if (outboundProxyAddress != null) { - outboundProxyAddress.transport = selectedTransport.value newParams.setRoutesAddresses(arrayOf(outboundProxyAddress)) } else { Log.e("$TAG Failed to parse outbound proxy server!") From b27ee6c4e3eec9eda00e045cd67e79092ab52d19 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 6 Jul 2026 10:45:07 +0200 Subject: [PATCH 585/593] Prevent black screen when pausing conference + prevent fullscreen mode while conference is paused + hide conference SIP URI in call ended screen --- .../viewmodel/ConferenceViewModel.kt | 5 ++++ .../call_active_conference_fragment.xml | 30 +++++++++++++++++++ .../main/res/layout/call_ended_fragment.xml | 2 +- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt index e028decfcf..1af6b1366d 100644 --- a/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/conference/viewmodel/ConferenceViewModel.kt @@ -392,6 +392,11 @@ class ConferenceViewModel return } + if (isPaused.value == true) { + // Do not allow turning full screen on while conference is paused + return + } + if (conferenceLayout.value == AUDIO_ONLY_LAYOUT) { // Do not allow turning full screen on for audio only conference return diff --git a/app/src/main/res/layout/call_active_conference_fragment.xml b/app/src/main/res/layout/call_active_conference_fragment.xml index 8547881cc6..35f2549c9e 100644 --- a/app/src/main/res/layout/call_active_conference_fragment.xml +++ b/app/src/main/res/layout/call_active_conference_fragment.xml @@ -270,6 +270,36 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="@id/hinge_bottom"/> + + + + + + + + Date: Mon, 6 Jul 2026 11:24:46 +0000 Subject: [PATCH 586/593] Updated translations from Weblate --- app/src/main/res/values-cs/strings.xml | 2 + app/src/main/res/values-pt-rBR/strings.xml | 7 ++ app/src/main/res/values-zh-rCN/strings.xml | 99 +++++++++++++++++++++- 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index aa722bfaa0..75f31c3267 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -936,4 +936,6 @@ Už nebudete moci odesílat ani přijímat nové zprávy, ale vaše historie zpráv zůstane k dispozici. Opravdu chcete smazat tento záznam hovoru? Opravdu chcete opustit tuto konverzaci? + Toto oznámení udržuje aplikaci aktivní, i když běží na pozadí + Pomocí senzoru přiblížení vypnout displej, když se přiblížíte k uchu diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 656f922073..d560391fca 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -846,4 +846,11 @@ Entendido &appName; notificações de erro da conta Clique nesta notificação para corrigí-la + Não cancele + Essa notificação mantém o aplicativo em execução em segundo plano + Desligue o alto-falante + Ligue o alto-falante + Seu interlocutor não o ouve! + Falha no registro da conta %s! + Abra &appName; para atualizar o registro diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 644473d668..1a70bc16ee 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -341,8 +341,8 @@ 暂时没有通话… 聊天 您真的要删除所有通话记录吗? - 所有通话都将从历史记录中删除 - 所有通话都将从历史记录中删除 + 所有通话都将从历史记录中删除。 + 所有通话都将从历史记录中删除。 暂时没有联系人… 目前没有SIP联系人… 修改过滤器 @@ -469,7 +469,7 @@ 删除管理员权限 查看联系人资料 添加到联系人 - 所有消息都将从历史记录中删除 + 所有消息都将从历史记录中删除。 历史记录已成功删除 %s加入了聊天 %s现在是管理员 @@ -825,4 +825,97 @@ 尚无法打开受密码保护的PDF 参与者 编辑 + 陶土 + 珊瑚 + <已编辑> + 除非你删除联系人权限,否则下次应用启动时它们会再次被导入 + 支持推送通知的域名列表(用逗号分隔) + 如果填写了这个字段,出站代理将会自动启用。留空则禁用它。 + 未加密的对话 + 此消息已被删除 + 未找到参与者 + 此消息已被删除 + 移除参与者? + 这个参与者将被移出群聊。 + 删除会议? + 这次会议只会从本设备的会议列表中移除。 + 请勿取消 + 账户错误通知 + 打开扬声器 + 关闭扬声器 + 账户%s注册失败! + 打开&appName;以刷新注册 + 端到端加密 + 矿物蓝 + 为所有人删除 + 停录 + 手机号验证不可用,请使用邮箱注册流程。 + 无法验证电话号码 + 点击通知以便修复 + 未找到孤立的认证信息 + 显示过去的会议 + 所有字段都必须填写 + LDAP 服务器已保存 + 将 LDAP 日志添加到 &appName; 的日志中 + 这个通讯录是只读的 + 早期媒体 + 再点击一次以启用开发者设置 + 显示高级通话统计 + 导入的联系人已被删除 + 清除的认证信息不再关联到任何账户 + + 已移除%s孤立身份验证信息 + + 连接状态 + 出站SIP代理 + 搜索结果已达上限,请优化你的搜索。 + 消息没有端到端加密,记得不要分享敏感信息哦! + 在这段对话中交换的消息可能会被除了你的通信对象以外的人拦截和阅读,保密性无法保证! + 已编辑 + 为我删除 + 你真的要删除这次聊天吗? + 所有消息也会从聊天记录中删除。 + 你真的想离开这个聊天吗? + 你将无法发送或接收新消息,但你的消息记录仍然可用。 + HDMI + ICE: %s + IP 家族: %s + 蜂蜜 + 勃艮第 + 薄荷 + 李子 + + 取消消息编辑 + 已理解 + 你真的想删除这个通话记录吗? + 只有这个条目会被删除。 + 拒绝 + 应答 + 通话录音已保存 + 麦克风 + 您的对话方没有在听! + 薰衣草 + LIME 算法(用逗号分隔) + 靠近耳朵时使用接近传感器关闭屏幕 + 打不开这个 PDF,文件可能已损坏 + 允许的值有:c25519、c448、c25519k512、c25519mlk512 和 c448mlk1024 + 不要在安卓通知中显示消息内容 + 联系人排序 + 使用 &appName; 联系编辑以获取本地联系人 + 隐藏没有 SIP 地址或电话号码的联系人 + 对DTMF使用RFC 2833 + 使用 SIP INFO 发送 DTMF + 高级通话设置 + 自动应答,双向视频已启用 + 自动应答 + 再点击两次以启用开发者设置 + 在通话中启用录音/播放音量VU表 + 切换账户 + 消息正在编辑 + 会议将被取消 + 你想给所有参与者发送通知吗? + 关闭菜单 + 应用后台保活通知 + 用邮箱注册 + 铃声选择器不可用! From 1e1ab74920fb409869b028fb2aa8978ddf18184d Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Mon, 6 Jul 2026 14:40:26 +0200 Subject: [PATCH 587/593] Show chosen emoji reaction as soon as it's been picked by the user (don't wait for it to be actually sent to display it) --- .../java/org/linphone/ui/main/chat/model/MessageModel.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt index 4a23534b87..f339ed274c 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/MessageModel.kt @@ -247,7 +247,7 @@ class MessageModel @WorkerThread override fun onReactionRemoved(message: ChatMessage, address: Address) { - Log.i("$TAG A reaction was removed for message with ID [$id]") + Log.i("$TAG A reaction from [${address.asStringUriOnly()}] was removed for message with ID [$id]") updateReactionsList() } @@ -360,6 +360,7 @@ class MessageModel val reaction = chatMessage.createReaction(emoji) reaction.send() } + updateReactionsList() dismissLongPressMenuEvent.postValue(Event(true)) } } @@ -637,7 +638,9 @@ class MessageModel ourReactionIndex.postValue(-1) } - reactions.postValue(reactionsList) + if (reactionsList != reactions.value) { + reactions.postValue(reactionsList) + } } @WorkerThread From 46f8e7477c4ccd748aa968b85173427d9e8c084b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 7 Jul 2026 11:18:30 +0200 Subject: [PATCH 588/593] Bumped version code to 6.2.3 & updated CHANGELOG --- CHANGELOG.md | 13 +++++++++++++ app/build.gradle.kts | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 423c220bda..c0e21046a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,19 @@ Group changes to describe their impact on the project, as follows: Fixed for any bug fixes. Security to invite users to upgrade in case of vulnerabilities. +## [6.2.3] - 2026-07-07 + +### Added +- Setting to disable proximity sensor turning screen off when device is next to the ear during audio calls + +### Changed +- Show chat message emoji reaction as soon as selected instead of waiting for message to be sent + +### Fixed +- Account registrar & outbound SIP proxy URIs transport that couldn't be changed +- Layout when conference is paused +- Prevent fullscreen mode when conference is paused + ## [6.2.2] - 2026-06-25 ### Changed diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 494b606b6e..4ebdaef889 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -37,7 +37,7 @@ if (crashlyticsAvailable) { println("Crashlytics has been disabled because either google-services.json file wasn't found or local Linphone SDK build folder isn't configured") } -var gitVersion = "6.2.2" +var gitVersion = "6.2.3" var gitBranch = "" try { val gitDescribe = ProcessBuilder() @@ -106,8 +106,8 @@ android { applicationId = packageName minSdk = 28 targetSdk = 37 - versionCode = 602002 // 6.02.002 - versionName = "6.2.2" + versionCode = 602003 // 6.02.003 + versionName = "6.2.3" manifestPlaceholders["appAuthRedirectScheme"] = packageName From 53e15e94d2b67e4c76cc376dfe9c10b71c7d5800 Mon Sep 17 00:00:00 2001 From: Kenny Stimson Date: Thu, 6 Aug 2026 13:23:01 -0700 Subject: [PATCH 589/593] Re-apply AN 6.2 view/policy customizations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SIP address visibility: - Hide informational SIP-URI displays (cruft for non-technical users), still toggle-managed. Add suppress_sip_addresses (functional blocks incl. contact picker) as a sibling to hide_sip_addresses (informational only; overridden by suppress). Splitting these re-enables hide=true safely — hide=true alone had emptied the picker and broke contact selection. - Add getDisplayAddress: username-only for the account's home domain (keyed on the account domain, since app/default_domain isn't provisioned), full URI for external SIP or when only_display_sip_uri_username is off. Video: force off (server can't carry it) via linphonerc_factory (capture/display off, no auto initiate/accept); drop imperative CoreContext override; hide the auto-answer-with-video toggle. Chat: allow voice-to-text by dropping upstream flagNoPersonalizedLearning. Cleanup: remove the recover-phone-account flow and other stale 6.0.0 merge-base artifacts unreferenced by 6.2. --- app/src/main/assets/linphonerc_factory | 8 + .../org/linphone/contacts/ContactsManager.kt | 6 +- .../java/org/linphone/core/CoreContext.kt | 14 - .../java/org/linphone/core/CorePreferences.kt | 6 +- ...verPhoneAccountCodeConfirmationFragment.kt | 100 ---- .../fragment/RecoverPhoneAccountFragment.kt | 183 ------- .../viewmodel/RecoverPhoneAccountViewModel.kt | 449 ------------------ .../ui/call/viewmodel/CurrentCallViewModel.kt | 7 +- .../viewmodel/ConversationInfoViewModel.kt | 7 +- .../viewmodel/ConversationsListViewModel.kt | 3 +- .../ui/main/history/model/CallLogModel.kt | 7 +- .../history/viewmodel/HistoryListViewModel.kt | 3 +- .../history/viewmodel/StartCallViewModel.kt | 4 - .../ConversationContactOrSuggestionModel.kt | 14 +- .../viewmodel/AddressSelectionViewModel.kt | 3 +- .../java/org/linphone/utils/LinphoneUtils.kt | 13 + app/src/main/res/drawable/arrow_green.xml | 21 - app/src/main/res/drawable/arrow_red.xml | 21 - app/src/main/res/drawable/files.xml | 9 - ...shape_squircle_gray_200_r15_background.xml | 5 - .../res/layout-land/call_ended_fragment.xml | 1 - ...hone_account_confirm_sms_code_fragment.xml | 215 --------- ...sistant_recover_phone_account_fragment.xml | 212 --------- .../main/res/layout/call_ended_fragment.xml | 1 - .../layout/chat_conversation_send_area.xml | 1 - .../res/layout/settings_advanced_calls.xml | 322 ------------- .../settings_advanced_calls_auto_answer.xml | 2 + 27 files changed, 38 insertions(+), 1599 deletions(-) delete mode 100644 app/src/main/java/org/linphone/ui/assistant/fragment/RecoverPhoneAccountCodeConfirmationFragment.kt delete mode 100644 app/src/main/java/org/linphone/ui/assistant/fragment/RecoverPhoneAccountFragment.kt delete mode 100644 app/src/main/java/org/linphone/ui/assistant/viewmodel/RecoverPhoneAccountViewModel.kt delete mode 100644 app/src/main/res/drawable/arrow_green.xml delete mode 100644 app/src/main/res/drawable/arrow_red.xml delete mode 100644 app/src/main/res/drawable/files.xml delete mode 100644 app/src/main/res/drawable/shape_squircle_gray_200_r15_background.xml delete mode 100644 app/src/main/res/layout/assistant_recover_phone_account_confirm_sms_code_fragment.xml delete mode 100644 app/src/main/res/layout/assistant_recover_phone_account_fragment.xml delete mode 100644 app/src/main/res/layout/settings_advanced_calls.xml diff --git a/app/src/main/assets/linphonerc_factory b/app/src/main/assets/linphonerc_factory index 558f8c7f70..cf21b3804c 100644 --- a/app/src/main/assets/linphonerc_factory +++ b/app/src/main/assets/linphonerc_factory @@ -36,6 +36,14 @@ android_monitor_audio_devices=0 displaytype=MSAndroidTextureDisplay auto_resize_preview_to_keep_ratio=1 max_conference_size=vga +# AccelerateNetworks: server infrastructure cannot carry video. Video is off by +# default (capture/display disabled, policy set to never auto-initiate/accept). +# UI hides itself via hideVideo = !core.isVideoEnabled; there is no toggle to turn +# it on and AN provisioning must not set these on. +capture=0 +display=0 +automatically_initiate=0 +automatically_accept=0 [misc] enable_basic_to_client_group_chat_room_migration=0 diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 33a84344db..6d92bf9021 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -807,7 +807,7 @@ fun Friend.getPerson(): Person { @WorkerThread fun Friend.getListOfSipAddresses(): ArrayList
{ val addressesList = arrayListOf
() - if (corePreferences.hideSipAddresses) return addressesList + if (corePreferences.suppressSipAddresses) return addressesList for (address in addresses) { if (addressesList.find { it.weakEqual(address) } == null) { @@ -822,7 +822,7 @@ fun Friend.getListOfSipAddresses(): ArrayList
{ fun Friend.getListOfSipAddressesAndPhoneNumbers(listener: ContactNumberOrAddressClickListener): ArrayList { val addressesAndNumbers = arrayListOf() - // Will return an empty list if corePreferences.hideSipAddresses == true + // Will return an empty list if corePreferences.suppressSipAddresses == true for (address in getListOfSipAddresses()) { if (LinphoneUtils.isSipAddressLinkedToPhoneNumberByPresence(this, address.asStringUriOnly())) { continue @@ -831,7 +831,7 @@ fun Friend.getListOfSipAddressesAndPhoneNumbers(listener: ContactNumberOrAddress val data = ContactNumberOrAddressModel( this, address, - address.asStringUriOnly(), + LinphoneUtils.getDisplayAddress(address), true, // SIP addresses are always enabled listener, true diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index 3cdbadc907..13c07fc4ba 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -697,20 +697,6 @@ class CoreContext } computeUserAgent() - - // AccelerateNetworks: our server infrastructure cannot carry video, so video is - // disabled at the Core level on every start. This is the media-layer guarantee: - // no video is negotiated even on inbound invites. It also drives the UI, since - // 6.2 binds video controls to hideVideo = !core.isVideoEnabled, so every video - // control hides automatically without per-layout edits. - core.isVideoCaptureEnabled = false - core.isVideoDisplayEnabled = false - val videoPolicy = core.videoActivationPolicy.clone() - videoPolicy.automaticallyInitiate = false - videoPolicy.automaticallyAccept = false - core.videoActivationPolicy = videoPolicy - Log.i("$TAG [AN] Video disabled at Core level (capture/display off, no auto initiate/accept)") - Log.i("$TAG Core has been configured with user-agent [${core.userAgent}], starting it") core.start() } diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 4c8b73a4a1..646ea689f9 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -380,9 +380,13 @@ class CorePreferences val onlyDisplaySipUriUsername: Boolean get() = config.getBool("ui", "only_display_sip_uri_username", true) + @get:AnyThread + val suppressSipAddresses: Boolean + get() = config.getBool("ui", "suppress_sip_addresses", false) + @get:AnyThread val hideSipAddresses: Boolean - get() = config.getBool("ui", "hide_sip_addresses", false) + get() = suppressSipAddresses || config.getBool("ui", "hide_sip_addresses", true) @get:AnyThread val disableChat: Boolean diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/RecoverPhoneAccountCodeConfirmationFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/RecoverPhoneAccountCodeConfirmationFragment.kt deleted file mode 100644 index 77715e7b18..0000000000 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/RecoverPhoneAccountCodeConfirmationFragment.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2010-2025 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.ui.assistant.fragment - -import android.content.ClipboardManager -import android.content.Context -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.UiThread -import androidx.navigation.fragment.findNavController -import androidx.navigation.navGraphViewModels -import org.linphone.R -import org.linphone.core.tools.Log -import org.linphone.databinding.AssistantRecoverPhoneAccountConfirmSmsCodeFragmentBinding -import org.linphone.ui.GenericFragment -import org.linphone.ui.assistant.viewmodel.RecoverPhoneAccountViewModel - -@UiThread -class RecoverPhoneAccountCodeConfirmationFragment : GenericFragment() { - companion object { - private const val TAG = "[Recover Phone Account Code Confirmation Fragment]" - } - - private lateinit var binding: AssistantRecoverPhoneAccountConfirmSmsCodeFragmentBinding - - private val viewModel: RecoverPhoneAccountViewModel by navGraphViewModels( - R.id.assistant_nav_graph - ) - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = AssistantRecoverPhoneAccountConfirmSmsCodeFragmentBinding.inflate(layoutInflater) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - binding.viewModel = viewModel - observeToastEvents(viewModel) - - binding.setBackClickListener { - goBack() - } - - viewModel.accountCreatedEvent.observe(viewLifecycleOwner) { - it.consume { identity -> - Log.i("$TAG Account [$identity] has been created, leaving assistant") - requireActivity().finish() - } - } - - // This won't work starting Android 10 as clipboard access is denied unless app has focus, - // which won't be the case when the SMS arrives unless it is added into clipboard from a notification - val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - clipboard.addPrimaryClipChangedListener { - val data = clipboard.primaryClip - if (data != null && data.itemCount > 0) { - val clip = data.getItemAt(0).text.toString() - if (clip.length == 4) { - Log.i( - "$TAG Found 4 digits [$clip] as primary clip in clipboard, using it and clear it" - ) - viewModel.smsCodeFirstDigit.value = clip[0].toString() - viewModel.smsCodeSecondDigit.value = clip[1].toString() - viewModel.smsCodeThirdDigit.value = clip[2].toString() - viewModel.smsCodeLastDigit.value = clip[3].toString() - clipboard.clearPrimaryClip() - } - } - } - } - - private fun goBack() { - findNavController().popBackStack() - } -} diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/RecoverPhoneAccountFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/RecoverPhoneAccountFragment.kt deleted file mode 100644 index 81f03ad133..0000000000 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/RecoverPhoneAccountFragment.kt +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright (c) 2010-2025 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.ui.assistant.fragment - -import android.content.Context -import android.os.Bundle -import android.telephony.TelephonyManager -import android.text.Editable -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.ArrayAdapter -import androidx.annotation.UiThread -import androidx.appcompat.widget.AppCompatTextView -import androidx.navigation.fragment.findNavController -import androidx.navigation.navGraphViewModels -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.core.tools.Log -import org.linphone.ui.GenericFragment -import org.linphone.databinding.AssistantRecoverPhoneAccountFragmentBinding -import org.linphone.ui.assistant.viewmodel.RecoverPhoneAccountViewModel -import org.linphone.utils.AppUtils -import org.linphone.utils.ConfirmationDialogModel -import org.linphone.utils.DialogUtils -import org.linphone.utils.PhoneNumberUtils -import kotlin.getValue - -@UiThread -class RecoverPhoneAccountFragment : GenericFragment() { - companion object { - private const val TAG = "[Recover Phone Account Fragment]" - } - - private lateinit var binding: AssistantRecoverPhoneAccountFragmentBinding - - private val viewModel: RecoverPhoneAccountViewModel by navGraphViewModels( - R.id.assistant_nav_graph - ) - - private val dropdownListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - val dialPlan = viewModel.dialPlansList[position] - Log.i( - "$TAG Selected dialplan updated [+${dialPlan.countryCallingCode}] / [${dialPlan.country}]" - ) - viewModel.selectedDialPlan.value = dialPlan - } - - override fun onNothingSelected(parent: AdapterView<*>?) { - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - binding = AssistantRecoverPhoneAccountFragmentBinding.inflate(layoutInflater) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - binding.viewModel = viewModel - observeToastEvents(viewModel) - - binding.setBackClickListener { - goBack() - } - - binding.phoneNumber.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable?) { - viewModel.phoneNumberError.value = "" - } - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - }) - - viewModel.normalizedPhoneNumberEvent.observe(viewLifecycleOwner) { - it.consume { number -> - Log.i("$TAG Showing confirmation dialog for phone number [$number]") - showPhoneNumberConfirmationDialog(number) - } - } - - viewModel.goToSmsValidationEvent.observe(viewLifecycleOwner) { - it.consume { - if (findNavController().currentDestination?.id == R.id.recoverPhoneAccountFragment) { - Log.i("$TAG Going to SMS code validation fragment") - val action = RecoverPhoneAccountFragmentDirections.actionRecoverPhoneAccountFragmentToRecoverPhoneAccountCodeConfirmationFragment() - findNavController().navigate(action) - } - } - } - - val telephonyManager = requireContext().getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - val countryIso = telephonyManager.networkCountryIso - coreContext.postOnCoreThread { - val fragmentContext = context ?: return@postOnCoreThread - - val adapter = object : ArrayAdapter( - fragmentContext, - R.layout.drop_down_item, - viewModel.dialPlansLabelList - ) { - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val view = convertView ?: super.getView(position, null, parent) - val label = viewModel.dialPlansShortLabelList[position] - (view as? AppCompatTextView)?.text = label - return view - } - } - adapter.setDropDownViewResource(R.layout.assistant_country_picker_dropdown_cell) - - val dialPlan = PhoneNumberUtils.getDeviceDialPlan(countryIso) - var default = 0 - if (dialPlan != null) { - viewModel.selectedDialPlan.postValue(dialPlan) - default = viewModel.dialPlansList.indexOf(dialPlan) - } - - coreContext.postOnMainThread { - binding.prefix.adapter = adapter - binding.prefix.setSelection(default) - binding.prefix.onItemSelectedListener = dropdownListener - } - } - } - - private fun goBack() { - findNavController().popBackStack() - } - - private fun showPhoneNumberConfirmationDialog(number: String) { - val label = AppUtils.getFormattedString(R.string.assistant_dialog_confirm_phone_number_message, number) - val model = ConfirmationDialogModel(label) - val dialog = DialogUtils.getAccountCreationPhoneNumberConfirmationDialog( - requireActivity(), - model - ) - - model.dismissEvent.observe(viewLifecycleOwner) { - it.consume { - Log.w("$TAG User dismissed the dialog, aborting recovery process") - dialog.dismiss() - } - } - - model.confirmEvent.observe(viewLifecycleOwner) { - it.consume { - Log.i("$TAG User confirmed the phone number, requesting account creation token & SMS code") - viewModel.startRecoveryProcess() - dialog.dismiss() - } - } - - dialog.show() - } -} diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/RecoverPhoneAccountViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/RecoverPhoneAccountViewModel.kt deleted file mode 100644 index 2290a10683..0000000000 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/RecoverPhoneAccountViewModel.kt +++ /dev/null @@ -1,449 +0,0 @@ -/* - * Copyright (c) 2010-2025 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.ui.assistant.viewmodel - -import androidx.annotation.UiThread -import androidx.annotation.WorkerThread -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.json.JSONException -import org.json.JSONObject -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.core.AccountCreator -import org.linphone.core.AccountCreatorListenerStub -import org.linphone.core.AccountManagerServices -import org.linphone.core.AccountManagerServicesRequest -import org.linphone.core.AccountManagerServicesRequestListenerStub -import org.linphone.core.Core -import org.linphone.core.CoreListenerStub -import org.linphone.core.DialPlan -import org.linphone.core.Dictionary -import org.linphone.core.Factory -import org.linphone.core.tools.Log -import org.linphone.ui.GenericViewModel -import org.linphone.utils.Event -import org.linphone.utils.LinphoneUtils -import java.util.Locale - -class RecoverPhoneAccountViewModel : GenericViewModel() { - companion object { - private const val TAG = "[Recover Phone Account ViewModel]" - - private const val TIME_TO_WAIT_FOR_PUSH_NOTIFICATION_WITH_ACCOUNT_CREATION_TOKEN = 5000 - } - - val pushNotificationsAvailable = MutableLiveData() - - val dialPlansLabelList = arrayListOf() - - val dialPlansShortLabelList = arrayListOf() - - val dialPlansList = arrayListOf() - - val selectedDialPlan = MutableLiveData() - - val phoneNumber = MutableLiveData() - - val phoneNumberError = MutableLiveData() - - val confirmationMessage = MutableLiveData() - - val smsCodeFirstDigit = MutableLiveData() - val smsCodeSecondDigit = MutableLiveData() - val smsCodeThirdDigit = MutableLiveData() - val smsCodeLastDigit = MutableLiveData() - - val operationInProgress = MutableLiveData() - - val recoverEnabled = MediatorLiveData() - - private var normalizedPhoneNumber: String? = null - val normalizedPhoneNumberEvent = MutableLiveData>() - - val goToSmsValidationEvent = MutableLiveData>() - - val accountCreatedEvent = MutableLiveData>() - - private lateinit var accountManagerServices: AccountManagerServices - private val accountManagerServicesListener = object : AccountManagerServicesRequestListenerStub() { - @WorkerThread - override fun onRequestSuccessful( - request: AccountManagerServicesRequest, - data: String? - ) { - Log.i("$TAG Request [$request] was successful, data is [$data]") - operationInProgress.postValue(false) - } - - @WorkerThread - override fun onRequestError( - request: AccountManagerServicesRequest, - statusCode: Int, - errorMessage: String?, - parameterErrors: Dictionary? - ) { - Log.e( - "$TAG Request [$request] returned an error with status code [$statusCode] and message [$errorMessage]" - ) - operationInProgress.postValue(false) - - if (!errorMessage.isNullOrEmpty()) { - showFormattedRedToast(errorMessage, R.drawable.warning_circle) - } - - when (request.type) { - AccountManagerServicesRequest.Type.SendAccountCreationTokenByPush -> { - Log.w("$TAG Cancelling job waiting for push notification") - waitingForFlexiApiPushToken = false - waitForPushJob?.cancel() - } - else -> { - } - } - recoverEnabled.postValue(true) - } - } - private var accountCreationToken: String? = null - - private var waitingForFlexiApiPushToken = false - private var waitForPushJob: Job? = null - - private lateinit var accountCreator: AccountCreator - private val accountCreatorListener = object : AccountCreatorListenerStub() { - @WorkerThread - override fun onRecoverAccount( - creator: AccountCreator, - status: AccountCreator.Status, - response: String? - ) { - Log.i("$TAG Recover account status is $status") - operationInProgress.postValue(false) - - if (status == AccountCreator.Status.RequestOk) { - goToSmsValidationEvent.postValue(Event(true)) - } else { - Log.e("$TAG Error in onRecoverAccount [${status.name}]") - showFormattedRedToast(status.name, R.drawable.warning_circle) - } - } - - @WorkerThread - override fun onLoginLinphoneAccount( - creator: AccountCreator, - status: AccountCreator.Status, - response: String? - ) { - Log.i("$TAG onLoginLinphoneAccount status is $status") - operationInProgress.postValue(false) - - if (status == AccountCreator.Status.RequestOk) { - if (!createAccountAndAuthInfo()) { - Log.e("$TAG Failed to create account object") - } - } else { - Log.e("$TAG Error in onRecoverAccount [${status.name}]") - showFormattedRedToast(status.name, R.drawable.warning_circle) - } - } - } - - private val coreListener = object : CoreListenerStub() { - @WorkerThread - override fun onPushNotificationReceived(core: Core, payload: String?) { - Log.i("$TAG Push received: [$payload]") - - val data = payload.orEmpty() - if (data.isNotEmpty()) { - try { - // This is because JSONObject.toString() done by the SDK will result in payload looking like {"custom-payload":"{\"token\":\"value\"}"} - val cleanPayload = data - .replace("\\\"", "\"") - .replace("\"{", "{") - .replace("}\"", "}") - Log.i("$TAG Cleaned payload is: [$cleanPayload]") - - val json = JSONObject(cleanPayload) - val customPayload = json.getJSONObject("custom-payload") - if (customPayload.has("token")) { - waitForPushJob?.cancel() - waitingForFlexiApiPushToken = false - operationInProgress.postValue(false) - - val token = customPayload.getString("token") - if (token.isNotEmpty()) { - accountCreationToken = token - Log.i( - "$TAG Extracted token [$accountCreationToken] from push payload, recovering account" - ) - requestSmsCode() - } else { - Log.e("$TAG Push payload JSON object has an empty 'token'!") - onFlexiApiTokenRequestError() - } - } else { - Log.e("$TAG Push payload JSON object has no 'token' key!") - onFlexiApiTokenRequestError() - } - } catch (e: JSONException) { - Log.e("$TAG Exception trying to parse push payload as JSON: [$e]") - onFlexiApiTokenRequestError() - } - } else { - Log.e("$TAG Push payload is null or empty, can't extract auth token!") - onFlexiApiTokenRequestError() - } - } - } - - init { - coreContext.postOnCoreThread { core -> - core.addListener(coreListener) - - pushNotificationsAvailable.postValue(LinphoneUtils.arePushNotificationsAvailable(core)) - - val dialPlans = Factory.instance().dialPlans.toList() - for (dialPlan in dialPlans) { - dialPlansList.add(dialPlan) - dialPlansLabelList.add( - "${dialPlan.flag} ${dialPlan.country} | +${dialPlan.countryCallingCode}" - ) - dialPlansShortLabelList.add( - "${dialPlan.flag} +${dialPlan.countryCallingCode}" - ) - } - - accountManagerServices = core.createAccountManagerServices() - accountManagerServices.language = Locale.getDefault().language // Returns en, fr, etc... - - accountCreator = core.createAccountCreator("https://subscribe.linphone.org/api/") - accountCreator.addListener(accountCreatorListener) - } - - recoverEnabled.addSource(selectedDialPlan) { - recoverEnabled.value = phoneNumber.value.orEmpty().isNotEmpty() && selectedDialPlan.value?.countryCallingCode.orEmpty().isNotEmpty() - } - recoverEnabled.addSource(phoneNumber) { - recoverEnabled.value = phoneNumber.value.orEmpty().isNotEmpty() && selectedDialPlan.value?.countryCallingCode.orEmpty().isNotEmpty() - } - } - - override fun onCleared() { - coreContext.postOnCoreThread { core -> - core.removeListener(coreListener) - accountCreator.removeListener(accountCreatorListener) - } - - super.onCleared() - } - - @UiThread - fun sendCode() { - coreContext.postOnCoreThread { - val dialPlan = selectedDialPlan.value - if (dialPlan == null) { - Log.e("$TAG No dial plan (country) selected!") - return@postOnCoreThread - } - val number = phoneNumber.value.orEmpty().trim() - val formattedPhoneNumber = dialPlan.formatPhoneNumber(number, false) - Log.i( - "$TAG Formatted phone number [$number] using dial plan [${dialPlan.country}] is [$formattedPhoneNumber]" - ) - - val message = coreContext.context.getString( - R.string.assistant_account_creation_sms_confirmation_explanation, - formattedPhoneNumber - ) - normalizedPhoneNumber = formattedPhoneNumber - confirmationMessage.postValue(message) - normalizedPhoneNumberEvent.postValue(Event(formattedPhoneNumber)) - } - } - - @WorkerThread - fun requestSmsCode() { - operationInProgress.postValue(true) - - coreContext.core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath) - accountCreator.domain = corePreferences.defaultDomain - - val dialPlan = selectedDialPlan.value - if (dialPlan == null) { - Log.e("$TAG No dial plan (country) selected!") - return - } - val number = phoneNumber.value.orEmpty().trim() - val countryCallingCode = dialPlan.countryCallingCode - var result = AccountCreator.PhoneNumberStatus.fromInt( - accountCreator.setPhoneNumber(number, countryCallingCode) - ) - if (result != AccountCreator.PhoneNumberStatus.Ok) { - Log.e( - "$TAG Error [$result] setting the phone number: $number with prefix: $countryCallingCode" - ) - phoneNumberError.postValue(result.name) - operationInProgress.postValue(false) - return - } - Log.i("$TAG Phone number is ${accountCreator.phoneNumber}") - - val result2 = accountCreator.setUsername(accountCreator.phoneNumber) - if (result2 != AccountCreator.UsernameStatus.Ok) { - Log.e( - "$TAG Error [${result2.name}] setting the username: ${accountCreator.phoneNumber}" - ) - phoneNumberError.postValue(result2.name) - operationInProgress.postValue(false) - return - } - Log.i("$TAG Username is ${accountCreator.username}") - - accountCreator.token = accountCreationToken - Log.i("$TAG Token is ${accountCreator.token}") - - val status = accountCreator.recoverAccount() - Log.i("$TAG Recover account returned $status") - if (status != AccountCreator.Status.RequestOk) { - operationInProgress.postValue(false) - Log.e("$TAG Error doing recoverAccount [${status.name}]") - showFormattedRedToast(status.name, R.drawable.warning_circle) - } - } - - @UiThread - fun validateCode() { - operationInProgress.value = true - - coreContext.postOnCoreThread { core -> - val code = - "${smsCodeFirstDigit.value.orEmpty().trim()}${smsCodeSecondDigit.value.orEmpty().trim()}${smsCodeThirdDigit.value.orEmpty().trim()}${smsCodeLastDigit.value.orEmpty().trim()}" - accountCreator.activationCode = code - val status = accountCreator.loginLinphoneAccount() - Log.i("$TAG Code [$code] validation result is $status") - if (status != AccountCreator.Status.RequestOk) { - operationInProgress.postValue(false) - Log.e("$TAG Error doing loginLinphoneAccount [${status.name}]") - showFormattedRedToast(status.name, R.drawable.warning_circle) - } - - // Reset code - smsCodeFirstDigit.postValue("") - smsCodeSecondDigit.postValue("") - smsCodeThirdDigit.postValue("") - smsCodeLastDigit.postValue("") - } - } - - @WorkerThread - private fun createAccountAndAuthInfo(): Boolean { - val account = accountCreator.createAccountInCore() - - if (account == null) { - Log.e("$TAG Account creator couldn't create account") - return false - } - coreContext.core.defaultAccount = account - - val username = account.params.identityAddress?.username.orEmpty() - Log.i("$TAG Account created with username [$username]") - accountCreatedEvent.postValue(Event(username)) - return true - } - - @UiThread - fun startRecoveryProcess() { - coreContext.postOnCoreThread { - requestFlexiApiToken() - } - } - - @WorkerThread - private fun requestFlexiApiToken() { - if (!coreContext.core.isPushNotificationAvailable) { - Log.e( - "$TAG Core says push notification aren't available, can't request a token from FlexiAPI" - ) - onFlexiApiTokenRequestError() - return - } - - operationInProgress.postValue(true) - recoverEnabled.postValue(false) - - val pushConfig = coreContext.core.pushNotificationConfig - if (pushConfig != null) { - val provider = pushConfig.provider - val param = pushConfig.param - val prid = pushConfig.prid - if (provider.isNullOrEmpty() || param.isNullOrEmpty() || prid.isNullOrEmpty()) { - Log.e( - "$TAG At least one mandatory push information (provider [$provider], param [$param], prid [$prid]) is missing!" - ) - onFlexiApiTokenRequestError() - return - } - - // Request an auth token, will be sent by push - val request = accountManagerServices.createSendAccountCreationTokenByPushRequest( - provider, - param, - prid - ) - request.addListener(accountManagerServicesListener) - request.submit() - - val waitFor = TIME_TO_WAIT_FOR_PUSH_NOTIFICATION_WITH_ACCOUNT_CREATION_TOKEN - waitingForFlexiApiPushToken = true - waitForPushJob?.cancel() - - Log.i("$TAG Waiting push with auth token for $waitFor ms") - waitForPushJob = viewModelScope.launch { - withContext(Dispatchers.IO) { - delay(waitFor.toLong()) - } - withContext(Dispatchers.Main) { - if (waitingForFlexiApiPushToken) { - waitingForFlexiApiPushToken = false - Log.e("$TAG Auth token wasn't received by push in [$waitFor] ms") - onFlexiApiTokenRequestError() - } - } - } - } else { - Log.e("$TAG No push configuration object in Core, shouldn't happen!") - onFlexiApiTokenRequestError() - } - } - - @WorkerThread - private fun onFlexiApiTokenRequestError() { - Log.e("$TAG Flexi API token request by push error!") - operationInProgress.postValue(false) - showRedToast(R.string.assistant_account_register_push_notification_not_received_error, R.drawable.warning_circle) - } -} diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt index eb2786c7be..93ceb5504e 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt @@ -1182,12 +1182,7 @@ class CurrentCallViewModel canBePaused.postValue(canCallBePaused()) val address = call.callLog.remoteAddress - val uri = if (corePreferences.onlyDisplaySipUriUsername) { - address.username ?: "" - } else { - LinphoneUtils.getAddressAsCleanStringUriOnly(address) - } - displayedAddress.postValue(uri) + displayedAddress.postValue(LinphoneUtils.getDisplayAddress(address)) val model = if (conferenceInfo != null) { coreContext.contactsManager.getContactAvatarModelForConferenceInfo(conferenceInfo) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt index 336471f7af..0ef333ac89 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt @@ -474,12 +474,7 @@ class ConversationInfoViewModel val firstParticipant = chatRoom.participants.firstOrNull() if (firstParticipant != null) { val address = firstParticipant.address - val uri = if (corePreferences.onlyDisplaySipUriUsername) { - address.username ?: "" - } else { - LinphoneUtils.getAddressAsCleanStringUriOnly(address) - } - sipUri.postValue(uri) + sipUri.postValue(LinphoneUtils.getDisplayAddress(address)) val friend = coreContext.contactsManager.findContactByAddress(address) if (friend == null) { diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationsListViewModel.kt index 39e7456233..0ad7410216 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationsListViewModel.kt @@ -340,7 +340,6 @@ class ConversationsListViewModel val suggestionsList = arrayListOf() val requestList = arrayListOf() - val defaultAccountDomain = LinphoneUtils.getDefaultAccount()?.params?.domain for (result in results) { val address = result.address val friend = result.friend @@ -376,7 +375,7 @@ class ConversationsListViewModel continue } - val model = ConversationContactOrSuggestionModel(address, defaultAccountDomain = defaultAccountDomain) { + val model = ConversationContactOrSuggestionModel(address) { coreContext.startAudioCall(address) } val avatarModel = getContactAvatarModelForAddress(address) diff --git a/app/src/main/java/org/linphone/ui/main/history/model/CallLogModel.kt b/app/src/main/java/org/linphone/ui/main/history/model/CallLogModel.kt index 2179a07c29..56df7e8724 100644 --- a/app/src/main/java/org/linphone/ui/main/history/model/CallLogModel.kt +++ b/app/src/main/java/org/linphone/ui/main/history/model/CallLogModel.kt @@ -23,7 +23,6 @@ import androidx.annotation.IntegerRes import androidx.annotation.UiThread import androidx.annotation.WorkerThread import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.core.CallLog import org.linphone.core.tools.Log @@ -97,11 +96,7 @@ class CallLogModel friendRefKey = friend.refKey friendExists = coreContext.contactsManager.isContactAvailable(friend) } - displayedAddress = if (corePreferences.onlyDisplaySipUriUsername) { - address.username ?: "" - } else { - sipUri - } + displayedAddress = LinphoneUtils.getDisplayAddress(address) iconResId = LinphoneUtils.getCallIconResId(callLog.status, callLog.dir) } diff --git a/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryListViewModel.kt b/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryListViewModel.kt index 9fea4f8a81..eddf951176 100644 --- a/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/history/viewmodel/HistoryListViewModel.kt @@ -221,7 +221,6 @@ class HistoryListViewModel val suggestionsList = arrayListOf() val requestList = arrayListOf() - val defaultAccountDomain = LinphoneUtils.getDefaultAccount()?.params?.domain for (result in results) { val address = result.address val friend = result.friend @@ -257,7 +256,7 @@ class HistoryListViewModel continue } - val model = ConversationContactOrSuggestionModel(address, defaultAccountDomain = defaultAccountDomain) { + val model = ConversationContactOrSuggestionModel(address) { coreContext.startAudioCall(address) } val avatarModel = getContactAvatarModelForAddress(address) diff --git a/app/src/main/java/org/linphone/ui/main/history/viewmodel/StartCallViewModel.kt b/app/src/main/java/org/linphone/ui/main/history/viewmodel/StartCallViewModel.kt index bb77fc5543..787bae29f9 100644 --- a/app/src/main/java/org/linphone/ui/main/history/viewmodel/StartCallViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/history/viewmodel/StartCallViewModel.kt @@ -85,10 +85,6 @@ class StartCallViewModel MutableLiveData() } - val initiateBlindTransferEvent: MutableLiveData>> by lazy { - MutableLiveData>>() - } - private val conferenceListener = object : ConferenceListenerStub() { @WorkerThread override fun onStateChanged(conference: Conference, newState: Conference.State?) { diff --git a/app/src/main/java/org/linphone/ui/main/model/ConversationContactOrSuggestionModel.kt b/app/src/main/java/org/linphone/ui/main/model/ConversationContactOrSuggestionModel.kt index ff0a4dbac1..83c09c95a7 100644 --- a/app/src/main/java/org/linphone/ui/main/model/ConversationContactOrSuggestionModel.kt +++ b/app/src/main/java/org/linphone/ui/main/model/ConversationContactOrSuggestionModel.kt @@ -22,7 +22,6 @@ package org.linphone.ui.main.model import androidx.annotation.UiThread import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData -import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.core.Address import org.linphone.core.Friend import org.linphone.ui.main.contacts.model.ContactAvatarModel @@ -36,7 +35,6 @@ class ConversationContactOrSuggestionModel val conversationId: String = "", conversationSubject: String? = null, val friend: Friend? = null, - val defaultAccountDomain: String? = null, private val onClicked: ((Address) -> Unit)? = null ) { val id = friend?.refKey ?: address.asStringUriOnly().hashCode() @@ -52,17 +50,7 @@ class ConversationContactOrSuggestionModel address.username ?: address.domain.orEmpty() } - val sipUri = if (!corePreferences.hideSipAddresses) { - // Hide SIP address and only show username for suggestions - // on the same domain as the currently selected account - if (!defaultAccountDomain.isNullOrEmpty() && defaultAccountDomain == address.domain) { - address.username - } else { - address.asStringUriOnly() - } - } else { - address.username - } + val sipUri = LinphoneUtils.getDisplayAddress(address) val initials = AppUtils.getInitials(conversationSubject ?: name) diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt index 053d198c98..ee952228c6 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/AddressSelectionViewModel.kt @@ -316,7 +316,6 @@ abstract class AddressSelectionViewModel arrayListOf() } - val defaultAccountDomain = LinphoneUtils.getDefaultAccount()?.params?.domain val favoritesList = arrayListOf() val domain = corePreferences.contactsFilter // Make a quick synchronous search for favorites (in case of total results exceed magic search limit to prevent missing ones) @@ -384,7 +383,7 @@ abstract class AddressSelectionViewModel continue } - val model = ConversationContactOrSuggestionModel(address, defaultAccountDomain = defaultAccountDomain) { + val model = ConversationContactOrSuggestionModel(address) { coreContext.startAudioCall(address) } val avatarModel = getContactAvatarModelForAddress(address) diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 33c5c815b8..dd2298050f 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -115,6 +115,19 @@ class LinphoneUtils { return "$scheme:$username@${address.domain}" } + @WorkerThread + fun getDisplayAddress(address: Address): String { + val username = address.username + if (!corePreferences.onlyDisplaySipUriUsername || username.isNullOrEmpty()) { + return getAddressAsCleanStringUriOnly(address) + } + val homeDomain = getDefaultAccount()?.params?.domain + if (address.domain == homeDomain || address.domain == corePreferences.defaultDomain) { + return username + } + return getAddressAsCleanStringUriOnly(address) + } + @WorkerThread fun getDisplayName(address: Address?): String { if (address == null) return "[null]" diff --git a/app/src/main/res/drawable/arrow_green.xml b/app/src/main/res/drawable/arrow_green.xml deleted file mode 100644 index 27f31c09c8..0000000000 --- a/app/src/main/res/drawable/arrow_green.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/drawable/arrow_red.xml b/app/src/main/res/drawable/arrow_red.xml deleted file mode 100644 index 6fd056270e..0000000000 --- a/app/src/main/res/drawable/arrow_red.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/drawable/files.xml b/app/src/main/res/drawable/files.xml deleted file mode 100644 index 41d445997c..0000000000 --- a/app/src/main/res/drawable/files.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/shape_squircle_gray_200_r15_background.xml b/app/src/main/res/drawable/shape_squircle_gray_200_r15_background.xml deleted file mode 100644 index 2b7fce2504..0000000000 --- a/app/src/main/res/drawable/shape_squircle_gray_200_r15_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/call_ended_fragment.xml b/app/src/main/res/layout-land/call_ended_fragment.xml index 91f0be904e..bb6b3b2087 100644 --- a/app/src/main/res/layout-land/call_ended_fragment.xml +++ b/app/src/main/res/layout-land/call_ended_fragment.xml @@ -74,7 +74,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/assistant_recover_phone_account_fragment.xml b/app/src/main/res/layout/assistant_recover_phone_account_fragment.xml deleted file mode 100644 index 8b0a602fc2..0000000000 --- a/app/src/main/res/layout/assistant_recover_phone_account_fragment.xml +++ /dev/null @@ -1,212 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/call_ended_fragment.xml b/app/src/main/res/layout/call_ended_fragment.xml index 4d303f9faa..b5d00ca209 100644 --- a/app/src/main/res/layout/call_ended_fragment.xml +++ b/app/src/main/res/layout/call_ended_fragment.xml @@ -69,7 +69,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/settings_advanced_calls_auto_answer.xml b/app/src/main/res/layout/settings_advanced_calls_auto_answer.xml index 215325f445..514ff61b60 100644 --- a/app/src/main/res/layout/settings_advanced_calls_auto_answer.xml +++ b/app/src/main/res/layout/settings_advanced_calls_auto_answer.xml @@ -81,6 +81,7 @@ style="@style/settings_title_style" android:onClick="@{() -> viewModel.toggleEnableAutoAnswerIncomingCallsWithVideoDirectionSendReceive()}" android:id="@+id/auto_answer_incoming_calls_video_title" + android:visibility="gone" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="16dp" @@ -98,6 +99,7 @@ style="@style/material_switch_style" android:id="@+id/auto_answer_incoming_calls_video_switch" android:onClick="@{() -> viewModel.toggleEnableAutoAnswerIncomingCallsWithVideoDirectionSendReceive()}" + android:visibility="gone" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" From 1627b1bd36fb392893018d7ccdd43875c0501f6f Mon Sep 17 00:00:00 2001 From: Kenny Stimson Date: Fri, 7 Aug 2026 16:17:27 -0700 Subject: [PATCH 590/593] Subscribe to directory extensions for presence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VCard4 import leaves each friend's send_subscribe off, and with no RLS address the list-level flag alone won't subscribe them — so provisioned directory extensions had no presence. After the VCard4 directory list syncs, enable per-friend subscribe on friends whose SIP address is on the account's home domain (extensions; matches the old inline subscribe=1 for short numbers) and update the list's subscriptions. Re-runs each sync since import recreates the friends. External SIP and PSTN (no SIP address) get none. --- .../org/linphone/contacts/ContactsManager.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 6d92bf9021..93f3e08ec1 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -259,6 +259,9 @@ class ContactsManager Log.i("$TAG Friend list [${friendList.displayName}] sync status changed to [$status]") when (status) { FriendList.SyncStatus.Successful -> { + if (friendList.type == FriendList.Type.VCard4) { + enableDirectoryPresenceSubscriptions(friendList) + } notifyContactsListChanged() } FriendList.SyncStatus.Failure -> { @@ -621,6 +624,26 @@ class ContactsManager } } + @WorkerThread + private fun enableDirectoryPresenceSubscriptions(friendList: FriendList) { + val homeDomain = LinphoneUtils.getDefaultAccount()?.params?.domain ?: return + var changed = false + for (friend in friendList.friends) { + if (friend.isSubscribesEnabled) continue + if (friend.addresses.any { it.domain == homeDomain }) { + friend.edit() + friend.setSubscribesEnabled(true) + friend.done() + changed = true + } + } + if (changed) { + friendList.isSubscriptionsEnabled = true + friendList.updateSubscriptions() + Log.i("$TAG Enabled presence subscriptions for directory extensions on [$homeDomain]") + } + } + @WorkerThread fun onCoreStopped(core: Core) { Log.w("$TAG Core has been stopped") From b2341363cbf64f8b5fe3e3447c20e7b045749ac8 Mon Sep 17 00:00:00 2001 From: Kenny Stimson Date: Tue, 11 Aug 2026 12:06:47 -0700 Subject: [PATCH 591/593] CI: send release/* builds to the Play alpha (preprod) track Only main (beta) and testing (alpha) were distributed; release/* branches built an artifact but went nowhere. Trigger the workflow on release/** and upload those builds to alpha, so release branches reach preprod without a prod (beta) release. --- .github/workflows/an-mobile-android.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/an-mobile-android.yml b/.github/workflows/an-mobile-android.yml index f9298bccb5..b15a5711c7 100644 --- a/.github/workflows/an-mobile-android.yml +++ b/.github/workflows/an-mobile-android.yml @@ -4,6 +4,7 @@ on: branches: - main - testing + - 'release/**' workflow_dispatch: concurrency: group: "${{ github.ref }}" @@ -42,6 +43,7 @@ jobs: export GOOGLE_APPLICATION_CREDENTIALS=./google-creds.json if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then python3 upload-app.py --track beta com.acceleratenetworks.mobile "$(./version.sh)"; fi if [[ "${{ github.ref }}" == "refs/heads/testing" ]]; then python3 upload-app.py --track alpha com.acceleratenetworks.mobile "$(./version.sh)"; fi + if [[ "${{ github.ref }}" == refs/heads/release/* ]]; then python3 upload-app.py --track alpha com.acceleratenetworks.mobile "$(./version.sh)"; fi - uses: actions/upload-artifact@v4.1.0 if: always() with: From b9ca7afe09234c53b1cafb518ff9bc3fbde83c00 Mon Sep 17 00:00:00 2001 From: Kenny Stimson Date: Tue, 25 Aug 2026 19:45:28 -0700 Subject: [PATCH 592/593] Block conference merge until all calls have established media Merging builds a local conference via Conference.addParticipants over every call. A call still connecting/ringing has no stable media stream, so joining it to the mixer aborts liblinphone (StreamsGroup::joinMixerSession() already joined!, a native SIGABRT that kills the process and strands the live calls). Guard mergeCallsIntoConference to require every call's media be established, and hide the merge button until then. --- .../ui/call/viewmodel/CallsViewModel.kt | 35 +++++++++++++++++++ .../main/res/layout/calls_list_fragment.xml | 2 +- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt index bd58e5cdb3..f90dc1532d 100644 --- a/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CallsViewModel.kt @@ -49,6 +49,8 @@ class CallsViewModel val allCallsIntoConference = MutableLiveData() + val canMergeCalls = MutableLiveData() + val showTopBar = MutableLiveData() val goToActiveCallEvent = MutableLiveData>() @@ -156,6 +158,7 @@ class CallsViewModel init { showTopBar.value = false + canMergeCalls.value = false coreContext.postOnCoreThread { core -> core.addListener(coreListener) @@ -220,6 +223,14 @@ class CallsViewModel @UiThread fun mergeCallsIntoConference() { coreContext.postOnCoreThread { core -> + if (!areAllCallsMergeable(core)) { + Log.w( + "$TAG Refusing to merge: not every call has established media, joining a connecting call to the mixer would abort liblinphone" + ) + showRedToast(R.string.conference_failed_to_merge_calls_into_conference_toast, R.drawable.warning_circle) + return@postOnCoreThread + } + val callsCount = core.callsNb val defaultAccount = LinphoneUtils.getDefaultAccount() val subject = if (defaultAccount != null && defaultAccount.params.audioVideoConferenceFactoryAddress != null) { @@ -296,6 +307,30 @@ class CallsViewModel } else if (core.callsNb == 1) { configureTopBarForSingleCallOrConference() } + + canMergeCalls.postValue(areAllCallsMergeable(core)) + } + + // Merging builds a local conference by joining each call's media StreamsGroup to a mixer. + // A call still connecting/ringing has no stable stream, so the join aborts liblinphone with + // "StreamsGroup::joinMixerSession() already joined !" (native SIGABRT, kills the process). + // Only allow the merge once every call's media is established. + @WorkerThread + private fun areAllCallsMergeable(core: Core): Boolean { + return core.callsNb > 1 && core.calls.all { isCallMediaEstablished(it.state) } + } + + private fun isCallMediaEstablished(state: Call.State): Boolean { + return when (state) { + Call.State.StreamsRunning, + Call.State.Paused, + Call.State.Pausing, + Call.State.PausedByRemote, + Call.State.Resuming, + Call.State.Updating, + Call.State.UpdatedByRemote -> true + else -> false + } } private fun configureTopBarForSingleCallOrConference() { diff --git a/app/src/main/res/layout/calls_list_fragment.xml b/app/src/main/res/layout/calls_list_fragment.xml index 74d5662dbe..d1356db67d 100644 --- a/app/src/main/res/layout/calls_list_fragment.xml +++ b/app/src/main/res/layout/calls_list_fragment.xml @@ -53,7 +53,7 @@ android:layout_height="0dp" android:src="@drawable/arrows_merge" android:contentDescription="@string/content_description_merge_calls_into_conference" - android:visibility="@{viewModel.callsCount > 1 ? View.VISIBLE : View.GONE}" + android:visibility="@{viewModel.canMergeCalls ? View.VISIBLE : View.GONE}" app:tint="?attr/color_main2_500" app:layout_constraintBottom_toBottomOf="@id/title" app:layout_constraintEnd_toEndOf="parent" From 74d8a68279f84fd5ffd21d7d619c55b75b9b1fe1 Mon Sep 17 00:00:00 2001 From: Kenny Stimson Date: Tue, 25 Aug 2026 19:48:07 -0700 Subject: [PATCH 593/593] Bump version to 6.2.10 Alpha already carries versionCode 60020009 (6.2.9) from the CI-to-alpha change; bump so the conference-merge crash fix uploads without a duplicate-version-code rejection. --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 04c32bd2e1..d0d103a5dc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -106,8 +106,8 @@ android { applicationId = packageName minSdk = 28 targetSdk = 37 - versionCode = 60020009 // 6.002.0009 - versionName = "6.2.9" + versionCode = 60020010 // 6.002.0010 + versionName = "6.2.10" manifestPlaceholders["appAuthRedirectScheme"] = packageName