diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ed65195d..48cf99dd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,17 +15,19 @@ Group changes to describe their impact on the project, as follows: ### Added - Chat messages emoji "reactions" -## [5.1.0] - Unreleased +## [5.1.0] - 2023-08-21 ### Added - Showing short term presence for contacts whom publish it + added setting to disable it (enabled by default for sip.linphone.org accounts) - Confirmation dialog before removing account - Attended transfer instead of blind transfer if there is more than 1 call -- Added emoji picker in chat room, and increase size of text if it only contains emojis -- Added hidden setting to disable video completely -- Added hidden setting to prevent adding / editing / removing native contacts -- Added hidden setting to protect settings access using account password +- Last sent message delivery status (IMDN) icon in chat rooms list +- Emoji picker in chat room, and increase size of text if it only contains emojis +- Hidden setting to disable video completely +- Hidden setting to prevent adding / editing / removing native contacts +- Hidden setting to protect settings access using account password - SIP URI in call can be selected using long press +- Dialog showing up asking for correct account password in case of failed authentication ### Changed - Switched Account Creator backend from XMLRPC to FlexiAPI, it now requires to be able to receive a push notification @@ -38,6 +40,35 @@ Group changes to describe their impact on the project, as follows: ### Fixed - Messages not marked as reply in basic chat room if sending more than 1 content +- Chat message video attachment display when failing to get a preview picture + +## [5.0.14] - 2023-06-20 + +### Changed +- SDK update only + +## [5.0.13] - 2023-06-15 + +### Changed +- SDK update only + +## [5.0.12] - 2023-05-23 + +### Fixed +- Crash if notification manager throws an exception +- Video preview not moving if call was started in audio only + +## [5.0.11] - 2023-05-09 + +### Fixed +- Wrong call displayed when hanging up a call while an incoming one is ringing +- Crash related to call history +- Crash due to wrongly format string +- Add/remove missing listener on FriendLists created after Core has been created + +### Changed +- Improved GSM call interruption +- Updated translations ## [5.0.11] - 2023-05-09 diff --git a/app/build.gradle b/app/build.gradle index a2498facf..44e9bd8b1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,16 +7,16 @@ plugins { } def appVersionName = "5.1.0" -def appVersionCode = 50090 +def appVersionCode = 51000 -static def getPackageName() { - return "org.linphone" -} +def packageName = "org.linphone" def firebaseAvailable = new File(projectDir.absolutePath +'/google-services.json').exists() def crashlyticsAvailable = new File(projectDir.absolutePath +'/google-services.json').exists() && new File(LinphoneSdkBuildDir + '/libs/').exists() && new File(LinphoneSdkBuildDir + '/libs-debug/').exists() +def extractNativeLibs = false + if (firebaseAvailable) { apply plugin: 'com.google.gms.google-services' } @@ -83,13 +83,13 @@ android { targetCompatibility = 17 } - compileSdkVersion 33 + compileSdkVersion 34 defaultConfig { minSdkVersion 23 - targetSdkVersion 33 + targetSdkVersion 34 versionCode appVersionCode versionName "${project.version}" - applicationId getPackageName() + applicationId packageName } applicationVariants.all { variant -> @@ -101,19 +101,19 @@ android { if (firebaseAvailable) { enableFirebaseService = "true" } + // See https://developer.android.com/studio/releases/gradle-plugin#3-6-0-behavior for why extractNativeLibs is set to true in debug flavor if (variant.buildType.name == "release" || variant.buildType.name == "releaseWithCrashlytics") { - variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address", - linphone_file_provider: getPackageName() + ".fileprovider", + variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + packageName + ".provider.sip_address", + linphone_file_provider: packageName + ".fileprovider", appLabel: "@string/app_name", - firebaseServiceEnabled: enableFirebaseService, - extractNativeLibs: "false"] + firebaseServiceEnabled: enableFirebaseService] } else { - variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address", - linphone_file_provider: getPackageName() + ".debug.fileprovider", + variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + packageName + ".provider.sip_address", + linphone_file_provider: packageName + ".debug.fileprovider", appLabel: "@string/app_name_debug", - firebaseServiceEnabled: enableFirebaseService, - extractNativeLibs: "true"] + firebaseServiceEnabled: enableFirebaseService] + extractNativeLibs = true } } @@ -137,9 +137,9 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' resValue "string", "linphone_app_branch", gitBranch.toString().trim() - resValue "string", "sync_account_type", getPackageName() + ".sync" - resValue "string", "file_provider", getPackageName() + ".fileprovider" - resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address" + resValue "string", "sync_account_type", packageName + ".sync" + resValue "string", "file_provider", packageName + ".fileprovider" + resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + packageName + ".provider.sip_address" if (!firebaseAvailable) { resValue "string", "gcm_defaultSenderId", "none" @@ -169,9 +169,9 @@ android { jniDebuggable true resValue "string", "linphone_app_branch", gitBranch.toString().trim() - resValue "string", "sync_account_type", getPackageName() + ".sync" - resValue "string", "file_provider", getPackageName() + ".debug.fileprovider" - resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address" + resValue "string", "sync_account_type", packageName + ".sync" + resValue "string", "file_provider", packageName + ".debug.fileprovider" + resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + packageName + ".provider.sip_address" resValue "bool", "crashlytics_enabled", crashlyticsAvailable.toString() if (!firebaseAvailable) { @@ -193,28 +193,35 @@ android { } namespace 'org.linphone' + packagingOptions { + jniLibs { + useLegacyPackaging extractNativeLibs + } + } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'androidx.core:core-ktx:1.10.0' + implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.core:core-splashscreen:1.0.1' - implementation 'androidx.emoji2:emoji2:1.4.0-beta02' - implementation 'androidx.emoji2:emoji2-emojipicker:1.4.0-beta02' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' implementation 'androidx.media:media:1.6.0' implementation "androidx.security:security-crypto-ktx:1.1.0-alpha06" - implementation "androidx.window:window:1.0.0" + implementation "androidx.window:window:1.1.0" - def nav_version = "2.5.3" + def emoji_version = "1.4.0-rc01" + implementation "androidx.emoji2:emoji2:$emoji_version" + implementation "androidx.emoji2:emoji2-emojipicker:$emoji_version" + + def nav_version = "2.6.0" implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version" implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0" implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "androidx.gridlayout:gridlayout:1.0.0" - implementation 'androidx.recyclerview:recyclerview:1.3.0' + implementation 'androidx.recyclerview:recyclerview:1.3.1' implementation 'androidx.drawerlayout:drawerlayout:1.2.0' // https://github.com/material-components/material-components-android/blob/master/LICENSE Apache v2.0 @@ -223,7 +230,7 @@ dependencies { implementation 'com.google.android.flexbox:flexbox:3.0.0' // https://github.com/coil-kt/coil/blob/main/LICENSE.txt Apache v2.0 - def coil_version = "2.3.0" + def coil_version = "2.4.0" implementation("io.coil-kt:coil:$coil_version") implementation("io.coil-kt:coil-gif:$coil_version") implementation("io.coil-kt:coil-svg:$coil_version") @@ -247,7 +254,7 @@ dependencies { implementation 'org.linphone:linphone-sdk-android:5.3+' // Only enable leak canary prior to release - //debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4' + // debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' } task generateContactsXml(type: Copy) { @@ -257,7 +264,7 @@ task generateContactsXml(type: Copy) { filter { line -> line .replaceAll('%%AUTO_GENERATED%%', 'This file has been automatically generated, do not edit or commit !') - .replaceAll('%%PACKAGE_NAME%%', getPackageName()) + .replaceAll('%%PACKAGE_NAME%%', packageName) } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0ec17de66..4e6f2a244 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -54,6 +54,12 @@ + + + + + @@ -121,6 +126,7 @@ + @@ -128,12 +134,6 @@ - - - - - - @@ -169,7 +169,14 @@ + + diff --git a/app/src/main/java/org/linphone/LinphoneApplication.kt b/app/src/main/java/org/linphone/LinphoneApplication.kt index 87e435932..2a08ce958 100644 --- a/app/src/main/java/org/linphone/LinphoneApplication.kt +++ b/app/src/main/java/org/linphone/LinphoneApplication.kt @@ -80,7 +80,8 @@ class LinphoneApplication : Application(), ImageLoaderFactory { context: Context, pushReceived: Boolean = false, service: CoreService? = null, - useAutoStartDescription: Boolean = false + useAutoStartDescription: Boolean = false, + skipCoreStart: Boolean = false ): Boolean { if (::coreContext.isInitialized && !coreContext.stopped) { Log.d("[Application] Skipping Core creation (push received? $pushReceived)") @@ -96,7 +97,9 @@ class LinphoneApplication : Application(), ImageLoaderFactory { service, useAutoStartDescription ) - coreContext.start() + if (!skipCoreStart) { + coreContext.start() + } return true } diff --git a/app/src/main/java/org/linphone/activities/GenericFragment.kt b/app/src/main/java/org/linphone/activities/GenericFragment.kt index 0d1f89fc0..ab8db20ef 100644 --- a/app/src/main/java/org/linphone/activities/GenericFragment.kt +++ b/app/src/main/java/org/linphone/activities/GenericFragment.kt @@ -83,7 +83,9 @@ abstract class GenericFragment : Fragment() { } } } catch (ise: IllegalStateException) { - Log.e("[Generic Fragment] ${getFragmentRealClassName()} Can't go back: $ise") + Log.e( + "[Generic Fragment] ${getFragmentRealClassName()}.handleOnBackPressed() Can't go back: $ise" + ) } } } @@ -137,7 +139,8 @@ abstract class GenericFragment : Fragment() { try { requireActivity().onBackPressedDispatcher.onBackPressed() } catch (ise: IllegalStateException) { - Log.e("[Generic Fragment] ${getFragmentRealClassName()} can't go back: $ise") + Log.w("[Generic Fragment] ${getFragmentRealClassName()}.goBack() can't go back: $ise") + onBackPressedCallback.handleOnBackPressed() } } diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/AccountLoginFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/AccountLoginFragment.kt index abee404c6..e9a35b94b 100644 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/AccountLoginFragment.kt +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/AccountLoginFragment.kt @@ -75,6 +75,10 @@ class AccountLoginFragment : AbstractPhoneFragment + viewModel.getCountryNameFromPrefix(internationalPrefix) + } + viewModel.goToSmsValidationEvent.observe( viewLifecycleOwner ) { diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/CountryPickerFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/CountryPickerFragment.kt index 9f7f10024..2e3d4dc72 100644 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/CountryPickerFragment.kt +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/CountryPickerFragment.kt @@ -29,7 +29,7 @@ import org.linphone.activities.assistant.adapters.CountryPickerAdapter import org.linphone.core.DialPlan import org.linphone.databinding.AssistantCountryPickerFragmentBinding -class CountryPickerFragment() : DialogFragment() { +class CountryPickerFragment : DialogFragment() { private var _binding: AssistantCountryPickerFragmentBinding? = null private val binding get() = _binding!! private lateinit var adapter: CountryPickerAdapter diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountCreationFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountCreationFragment.kt index 005a8d8d1..4f285e8b5 100644 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountCreationFragment.kt +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountCreationFragment.kt @@ -62,6 +62,10 @@ class PhoneAccountCreationFragment : countryPickerFragment.show(childFragmentManager, "CountryPicker") } + viewModel.prefix.observe(viewLifecycleOwner) { internationalPrefix -> + viewModel.getCountryNameFromPrefix(internationalPrefix) + } + viewModel.goToSmsValidationEvent.observe( viewLifecycleOwner ) { diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt index c543ebc94..d8fd87e22 100644 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt @@ -73,6 +73,10 @@ class PhoneAccountLinkingFragment : AbstractPhoneFragment + viewModel.getCountryNameFromPrefix(internationalPrefix) + } + viewModel.goToSmsValidationEvent.observe( viewLifecycleOwner ) { diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountValidationFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountValidationFragment.kt index 28732a224..edcf18210 100644 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountValidationFragment.kt +++ b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountValidationFragment.kt @@ -34,6 +34,7 @@ import org.linphone.activities.assistant.viewmodels.PhoneAccountValidationViewMo import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel import org.linphone.activities.navigateToAccountSettings import org.linphone.activities.navigateToEchoCancellerCalibration +import org.linphone.compatibility.Compatibility import org.linphone.core.tools.Log import org.linphone.databinding.AssistantPhoneAccountValidationFragmentBinding @@ -113,7 +114,7 @@ class PhoneAccountValidationFragment : GenericFragment() + val prefixError = MutableLiveData() val phoneNumber = MutableLiveData() val phoneNumberError = MutableLiveData() @@ -49,7 +49,10 @@ abstract class AbstractPhoneViewModel(accountCreator: AccountCreator) : } fun isPhoneNumberOk(): Boolean { - return prefix.value.orEmpty().isNotEmpty() && phoneNumber.value.orEmpty().isNotEmpty() && phoneNumberError.value.orEmpty().isEmpty() + return prefix.value.orEmpty().length > 1 && // Not just '+' character + prefixError.value.orEmpty().isEmpty() && + phoneNumber.value.orEmpty().isNotEmpty() && + phoneNumberError.value.orEmpty().isEmpty() } fun updateFromPhoneNumberAndOrDialPlan(number: String?, dialPlan: DialPlan?) { @@ -70,7 +73,7 @@ abstract class AbstractPhoneViewModel(accountCreator: AccountCreator) : } } - private fun getCountryNameFromPrefix(prefix: String?) { + fun getCountryNameFromPrefix(prefix: String?) { if (!prefix.isNullOrEmpty()) { val countryCode = if (prefix.first() == '+') prefix.substring(1) else prefix val dialPlan = PhoneNumberUtils.getDialPlanFromCountryCallingPrefix(countryCode) diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/AccountLoginViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/AccountLoginViewModel.kt index 807ed80cc..579a76eb7 100644 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/AccountLoginViewModel.kt +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/AccountLoginViewModel.kt @@ -46,7 +46,7 @@ class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewM val password = MutableLiveData() val passwordError = MutableLiveData() - val loginEnabled: MediatorLiveData = MediatorLiveData() + val loginEnabled = MediatorLiveData() val waitForServerAnswer = MutableLiveData() @@ -140,6 +140,9 @@ class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewM loginEnabled.addSource(phoneNumberError) { loginEnabled.value = isLoginButtonEnabled() } + loginEnabled.addSource(prefixError) { + loginEnabled.value = isLoginButtonEnabled() + } } override fun onCleared() { @@ -149,6 +152,7 @@ class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewM override fun onFlexiApiTokenReceived() { Log.i("[Assistant] [Account Login] Using FlexiAPI auth token [${accountCreator.token}]") + waitForServerAnswer.value = false loginWithPhoneNumber() } @@ -161,10 +165,19 @@ class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewM fun removeInvalidProxyConfig() { val account = accountToCheck account ?: return + + val core = coreContext.core val authInfo = account.findAuthInfo() - if (authInfo != null) coreContext.core.removeAuthInfo(authInfo) - coreContext.core.removeAccount(account) + if (authInfo != null) core.removeAuthInfo(authInfo) + core.removeAccount(account) accountToCheck = null + + // Make sure there is a valid default account + val accounts = core.accountList + if (accounts.isNotEmpty() && core.defaultAccount == null) { + core.defaultAccount = accounts.first() + core.refreshRegisters() + } } fun continueEvenIfInvalidCredentials() { diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/EchoCancellerCalibrationViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EchoCancellerCalibrationViewModel.kt index 9b5c54df8..09d01c069 100644 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/EchoCancellerCalibrationViewModel.kt +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EchoCancellerCalibrationViewModel.kt @@ -51,16 +51,16 @@ class EchoCancellerCalibrationViewModel : ViewModel() { coreContext.core.removeListener(listener) when (status) { EcCalibratorStatus.DoneNoEcho -> { - Log.i("[Echo Canceller Calibration] Done, no echo") + Log.i("[Assistant] [Echo Canceller Calibration] Done, no echo") } EcCalibratorStatus.Done -> { - Log.i("[Echo Canceller Calibration] Done, delay is ${delay}ms") + Log.i("[Assistant] [Echo Canceller Calibration] Done, delay is ${delay}ms") } EcCalibratorStatus.Failed -> { - Log.w("[Echo Canceller Calibration] Failed") + Log.w("[Assistant] [Echo Canceller Calibration] Failed") } EcCalibratorStatus.InProgress -> { - Log.i("[Echo Canceller Calibration] In progress") + Log.i("[Assistant] [Echo Canceller Calibration] In progress") } } echoCalibrationTerminated.value = Event(true) diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountCreationViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountCreationViewModel.kt index 08b6f4ab7..dea25074d 100644 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountCreationViewModel.kt +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountCreationViewModel.kt @@ -72,7 +72,7 @@ class EmailAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPu status: AccountCreator.Status, response: String? ) { - Log.i("[Account Creation] onIsAccountExist status is $status") + Log.i("[Assistant] [Account Creation] onIsAccountExist status is $status") when (status) { AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> { waitForServerAnswer.value = false @@ -99,7 +99,7 @@ class EmailAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPu status: AccountCreator.Status, response: String? ) { - Log.i("[Account Creation] onCreateAccount status is $status") + Log.i("[Assistant] [Account Creation] onCreateAccount status is $status") waitForServerAnswer.value = false when (status) { @@ -149,11 +149,11 @@ class EmailAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPu } override fun onFlexiApiTokenReceived() { - Log.i("[Account Creation] Using FlexiAPI auth token [${accountCreator.token}]") + Log.i("[Assistant] [Account Creation] Using FlexiAPI auth token [${accountCreator.token}]") waitForServerAnswer.value = true val status = accountCreator.isAccountExist - Log.i("[Account Creation] Account exists returned $status") + Log.i("[Assistant] [Account Creation] Account exists returned $status") if (status != AccountCreator.Status.RequestOk) { waitForServerAnswer.value = false onErrorEvent.value = Event("Error: ${status.name}") @@ -161,7 +161,7 @@ class EmailAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPu } override fun onFlexiApiTokenRequestError() { - Log.e("[Account Creation] Failed to get an auth token from FlexiAPI") + Log.e("[Assistant] [Account Creation] Failed to get an auth token from FlexiAPI") waitForServerAnswer.value = false onErrorEvent.value = Event("Error: Failed to get an auth token from account manager server") } @@ -175,11 +175,11 @@ class EmailAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPu val token = accountCreator.token.orEmpty() if (token.isNotEmpty()) { Log.i( - "[Account Creation] We already have an auth token from FlexiAPI [$token], continue" + "[Assistant] [Account Creation] We already have an auth token from FlexiAPI [$token], continue" ) onFlexiApiTokenReceived() } else { - Log.i("[Account Creation] Requesting an auth token from FlexiAPI") + Log.i("[Assistant] [Account Creation] Requesting an auth token from FlexiAPI") waitForServerAnswer.value = true requestFlexiApiToken() } diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountValidationViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountValidationViewModel.kt index 52fffbb26..bc72fbc90 100644 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountValidationViewModel.kt +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountValidationViewModel.kt @@ -57,7 +57,7 @@ class EmailAccountValidationViewModel(val accountCreator: AccountCreator) : View status: AccountCreator.Status, response: String? ) { - Log.i("[Account Validation] onIsAccountActivated status is $status") + Log.i("[Assistant] [Account Validation] onIsAccountActivated status is $status") waitForServerAnswer.value = false when (status) { diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/GenericLoginViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/GenericLoginViewModel.kt index 0a6c75014..5e0e50dfe 100644 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/GenericLoginViewModel.kt +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/GenericLoginViewModel.kt @@ -108,10 +108,19 @@ class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewMo fun removeInvalidProxyConfig() { val account = accountToCheck account ?: return + + val core = coreContext.core val authInfo = account.findAuthInfo() - if (authInfo != null) coreContext.core.removeAuthInfo(authInfo) - coreContext.core.removeAccount(account) + if (authInfo != null) core.removeAuthInfo(authInfo) + core.removeAccount(account) accountToCheck = null + + // Make sure there is a valid default account + val accounts = core.accountList + if (accounts.isNotEmpty() && core.defaultAccount == null) { + core.defaultAccount = accounts.first() + core.refreshRegisters() + } } fun continueEvenIfInvalidCredentials() { @@ -143,6 +152,8 @@ class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewMo } private fun isLoginButtonEnabled(): Boolean { - return username.value.orEmpty().isNotEmpty() && domain.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty() + return username.value.orEmpty().isNotEmpty() && + domain.value.orEmpty().isNotEmpty() && + password.value.orEmpty().isNotEmpty() } } diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountCreationViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountCreationViewModel.kt index a857cd1c3..03b6face3 100644 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountCreationViewModel.kt +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountCreationViewModel.kt @@ -68,7 +68,7 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh status: AccountCreator.Status, response: String? ) { - Log.i("[Phone Account Creation] onIsAccountExist status is $status") + Log.i("[Assistant] [Phone Account Creation] onIsAccountExist status is $status") when (status) { AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> { waitForServerAnswer.value = false @@ -92,7 +92,7 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh status: AccountCreator.Status, response: String? ) { - Log.i("[Phone Account Creation] onIsAliasUsed status is $status") + Log.i("[Assistant] [Phone Account Creation] onIsAliasUsed status is $status") when (status) { AccountCreator.Status.AliasExist -> { waitForServerAnswer.value = false @@ -114,7 +114,9 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh } AccountCreator.Status.AliasNotExist -> { val createAccountStatus = creator.createAccount() - Log.i("[Phone Account Creation] createAccount returned $createAccountStatus") + Log.i( + "[Assistant] [Phone Account Creation] createAccount returned $createAccountStatus" + ) if (createAccountStatus != AccountCreator.Status.RequestOk) { waitForServerAnswer.value = false onErrorEvent.value = Event("Error: ${status.name}") @@ -132,7 +134,7 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh status: AccountCreator.Status, response: String? ) { - Log.i("[Phone Account Creation] onCreateAccount status is $status") + Log.i("[Assistant] [Phone Account Creation] onCreateAccount status is $status") waitForServerAnswer.value = false when (status) { AccountCreator.Status.AccountCreated -> { @@ -173,6 +175,9 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh createEnabled.addSource(phoneNumberError) { createEnabled.value = isCreateButtonEnabled() } + createEnabled.addSource(prefixError) { + createEnabled.value = isCreateButtonEnabled() + } } override fun onCleared() { @@ -181,9 +186,22 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh } override fun onFlexiApiTokenReceived() { - Log.i("[Phone Account Creation] Using FlexiAPI auth token [${accountCreator.token}]") + Log.i( + "[Assistant] [Phone Account Creation] Using FlexiAPI auth token [${accountCreator.token}]" + ) accountCreator.displayName = displayName.value - accountCreator.setPhoneNumber(phoneNumber.value, prefix.value) + + val result = AccountCreator.PhoneNumberStatus.fromInt( + accountCreator.setPhoneNumber(phoneNumber.value, prefix.value) + ) + if (result != AccountCreator.PhoneNumberStatus.Ok) { + Log.e( + "[Assistant] [Phone Account Creation] Error [$result] setting the phone number: ${phoneNumber.value} with prefix: ${prefix.value}" + ) + phoneNumberError.value = result.name + return + } + Log.i("[Assistant] [Phone Account Creation] Phone number is ${accountCreator.phoneNumber}") if (useUsername.value == true) { accountCreator.username = username.value @@ -199,14 +217,14 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh } override fun onFlexiApiTokenRequestError() { - Log.e("[Phone Account Creation] Failed to get an auth token from FlexiAPI") + Log.e("[Assistant] [Phone Account Creation] Failed to get an auth token from FlexiAPI") waitForServerAnswer.value = false onErrorEvent.value = Event("Error: Failed to get an auth token from account manager server") } private fun checkUsername() { val status = accountCreator.isAccountExist - Log.i("[Phone Account Creation] isAccountExist returned $status") + Log.i("[Assistant] [Phone Account Creation] isAccountExist returned $status") if (status != AccountCreator.Status.RequestOk) { waitForServerAnswer.value = false onErrorEvent.value = Event("Error: ${status.name}") @@ -215,7 +233,7 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh private fun checkPhoneNumber() { val status = accountCreator.isAliasUsed - Log.i("[Phone Account Creation] isAliasUsed returned $status") + Log.i("[Assistant] [Phone Account Creation] isAliasUsed returned $status") if (status != AccountCreator.Status.RequestOk) { waitForServerAnswer.value = false onErrorEvent.value = Event("Error: ${status.name}") @@ -226,11 +244,11 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh val token = accountCreator.token.orEmpty() if (token.isNotEmpty()) { Log.i( - "[Phone Account Creation] We already have an auth token from FlexiAPI [$token], continue" + "[Assistant] [Phone Account Creation] We already have an auth token from FlexiAPI [$token], continue" ) onFlexiApiTokenReceived() } else { - Log.i("[Phone Account Creation] Requesting an auth token from FlexiAPI") + Log.i("[Assistant] [Phone Account Creation] Requesting an auth token from FlexiAPI") waitForServerAnswer.value = true requestFlexiApiToken() } diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountLinkingViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountLinkingViewModel.kt index b38b8e86d..78c11c04e 100644 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountLinkingViewModel.kt +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountLinkingViewModel.kt @@ -60,12 +60,12 @@ class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPho status: AccountCreator.Status, response: String? ) { - Log.i("[Phone Account Linking] onIsAliasUsed status is $status") + Log.i("[Assistant] [Phone Account Linking] onIsAliasUsed status is $status") when (status) { AccountCreator.Status.AliasNotExist -> { if (creator.linkAccount() != AccountCreator.Status.RequestOk) { - Log.e("[Phone Account Linking] linkAccount status is $status") + Log.e("[Assistant] [Phone Account Linking] linkAccount status is $status") waitForServerAnswer.value = false onErrorEvent.value = Event("Error: ${status.name}") } @@ -86,7 +86,7 @@ class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPho status: AccountCreator.Status, response: String? ) { - Log.i("[Phone Account Linking] onLinkAccount status is $status") + Log.i("[Assistant] [Phone Account Linking] onLinkAccount status is $status") waitForServerAnswer.value = false when (status) { @@ -113,6 +113,9 @@ class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPho linkEnabled.addSource(phoneNumberError) { linkEnabled.value = isLinkButtonEnabled() } + linkEnabled.addSource(prefixError) { + linkEnabled.value = isLinkButtonEnabled() + } } override fun onCleared() { @@ -123,10 +126,10 @@ class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPho override fun onFlexiApiTokenReceived() { accountCreator.setPhoneNumber(phoneNumber.value, prefix.value) accountCreator.username = username.value - Log.i("[Phone Account Linking] Phone number is ${accountCreator.phoneNumber}") + Log.i("[Assistant] [Phone Account Linking] Phone number is ${accountCreator.phoneNumber}") val status: AccountCreator.Status = accountCreator.isAliasUsed - Log.i("[Phone Account Linking] isAliasUsed returned $status") + Log.i("[Assistant] [Phone Account Linking] isAliasUsed returned $status") if (status != AccountCreator.Status.RequestOk) { waitForServerAnswer.value = false onErrorEvent.value = Event("Error: ${status.name}") @@ -134,12 +137,12 @@ class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPho } override fun onFlexiApiTokenRequestError() { - Log.e("[Phone Account Linking] Failed to get an auth token from FlexiAPI") + Log.e("[Assistant] [Phone Account Linking] Failed to get an auth token from FlexiAPI") waitForServerAnswer.value = false } fun link() { - Log.i("[Phone Account Linking] Requesting an auth token from FlexiAPI") + Log.i("[Assistant] [Phone Account Linking] Requesting an auth token from FlexiAPI") waitForServerAnswer.value = true requestFlexiApiToken() } diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/QrCodeViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/QrCodeViewModel.kt index b5daaec35..c0f988d0f 100644 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/QrCodeViewModel.kt +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/QrCodeViewModel.kt @@ -34,7 +34,7 @@ class QrCodeViewModel : ViewModel() { private val listener = object : CoreListenerStub() { override fun onQrcodeFound(core: Core, result: String?) { - Log.i("[QR Code] Found [$result]") + Log.i("[Assistant] [QR Code] Found [$result]") if (result != null) qrCodeFoundEvent.postValue(Event(result)) } } @@ -54,7 +54,7 @@ class QrCodeViewModel : ViewModel() { for (camera in coreContext.core.videoDevicesList) { if (camera.contains("Back")) { - Log.i("[QR Code] Found back facing camera: $camera") + Log.i("[Assistant] [QR Code] Found back facing camera: $camera") coreContext.core.videoDevice = camera return } @@ -62,7 +62,7 @@ class QrCodeViewModel : ViewModel() { val first = coreContext.core.videoDevicesList.firstOrNull() if (first != null) { - Log.i("[QR Code] Using first camera found: $first") + Log.i("[Assistant] [QR Code] Using first camera found: $first") coreContext.core.videoDevice = first } } diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/RemoteProvisioningViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/RemoteProvisioningViewModel.kt index 580e04a38..7a993d097 100644 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/RemoteProvisioningViewModel.kt +++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/RemoteProvisioningViewModel.kt @@ -77,7 +77,7 @@ class RemoteProvisioningViewModel : ViewModel() { fun fetchAndApply() { val url = urlToFetch.value.orEmpty() coreContext.core.provisioningUri = url - Log.w("[Remote Provisioning] Url set to [$url], restarting Core") + Log.w("[Assistant] [Remote Provisioning] Url set to [$url], restarting Core") fetchInProgress.value = true coreContext.core.stop() coreContext.core.start() diff --git a/app/src/main/java/org/linphone/activities/main/MainActivity.kt b/app/src/main/java/org/linphone/activities/main/MainActivity.kt index f170c5be5..1fc446783 100644 --- a/app/src/main/java/org/linphone/activities/main/MainActivity.kt +++ b/app/src/main/java/org/linphone/activities/main/MainActivity.kt @@ -35,6 +35,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.doOnAttach import androidx.databinding.DataBindingUtil import androidx.fragment.app.FragmentContainerView +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController @@ -58,6 +59,10 @@ import org.linphone.activities.main.viewmodels.SharedMainViewModel import org.linphone.activities.navigateToDialer import org.linphone.compatibility.Compatibility import org.linphone.contact.ContactsUpdatedListenerStub +import org.linphone.core.AuthInfo +import org.linphone.core.AuthMethod +import org.linphone.core.Core +import org.linphone.core.CoreListenerStub import org.linphone.core.CorePreferences import org.linphone.core.tools.Log import org.linphone.databinding.MainActivityBinding @@ -108,6 +113,25 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin private var shouldTabsBeVisibleDependingOnDestination = true private var shouldTabsBeVisibleDueToOrientationAndKeyboard = true + private val authenticationRequestedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + private val coreListener: CoreListenerStub = object : CoreListenerStub() { + override fun onAuthenticationRequested(core: Core, authInfo: AuthInfo, method: AuthMethod) { + if (authInfo.username == null || authInfo.domain == null || authInfo.realm == null) { + return + } + + Log.w( + "[Main Activity] Authentication requested for account [${authInfo.username}@${authInfo.domain}] with realm [${authInfo.realm}] using method [$method]" + ) + authenticationRequestedEvent.value = Event(authInfo) + } + } + + private val keyboardVisibilityListeners = arrayListOf() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -143,6 +167,14 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin } } + authenticationRequestedEvent.observe( + this + ) { + it.consume { authInfo -> + showAuthenticationRequestedDialog(authInfo) + } + } + if (coreContext.core.accountList.isEmpty()) { if (corePreferences.firstStart) { startActivity(Intent(this, AssistantActivity::class.java)) @@ -174,9 +206,11 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin override fun onResume() { super.onResume() coreContext.contactsManager.addListener(listener) + coreContext.core.addListener(coreListener) } override fun onPause() { + coreContext.core.removeListener(coreListener) coreContext.contactsManager.removeListener(listener) super.onPause() } @@ -205,13 +239,17 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin registerComponentCallbacks(componentCallbacks) findNavController(R.id.nav_host_fragment).addOnDestinationChangedListener(this) - binding.rootCoordinatorLayout.addKeyboardInsetListener { keyboardVisible -> + binding.rootCoordinatorLayout.setKeyboardInsetListener { keyboardVisible -> val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE Log.i( "[Main Activity] Keyboard is ${if (keyboardVisible) "visible" else "invisible"}, orientation is ${if (portraitOrientation) "portrait" else "landscape"}" ) shouldTabsBeVisibleDueToOrientationAndKeyboard = !portraitOrientation || !keyboardVisible updateTabsFragmentVisibility() + + for (listener in keyboardVisibilityListeners) { + listener.onKeyboardVisibilityChanged(keyboardVisible) + } } initOverlay() @@ -246,6 +284,14 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin updateTabsFragmentVisibility() } + fun addKeyboardVisibilityListener(listener: AppUtils.KeyboardVisibilityListener) { + keyboardVisibilityListeners.add(listener) + } + + fun removeKeyboardVisibilityListener(listener: AppUtils.KeyboardVisibilityListener) { + keyboardVisibilityListeners.remove(listener) + } + fun hideKeyboard() { currentFocus?.hideKeyboard() } @@ -343,7 +389,12 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin // Prevent this intent to be processed again intent.action = null intent.data = null - intent.extras?.clear() + val extras = intent.extras + if (extras != null) { + for (key in extras.keySet()) { + intent.removeExtra(key) + } + } } private fun handleMainIntent(intent: Intent) { @@ -420,6 +471,8 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin } } + addressToCall = addressToCall.replace("%40", "@") + val address = coreContext.core.interpretUrl( addressToCall, LinphoneUtils.applyInternationalPrefix() @@ -630,4 +683,43 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin dialog.show() } + + private fun showAuthenticationRequestedDialog( + authInfo: AuthInfo + ) { + val identity = "${authInfo.username}@${authInfo.domain}" + Log.i("[Main Activity] Showing authentication required dialog for account [$identity]") + + val dialogViewModel = DialogViewModel( + getString(R.string.dialog_authentication_required_message, identity), + getString(R.string.dialog_authentication_required_title) + ) + dialogViewModel.showPassword = true + dialogViewModel.passwordTitle = getString( + R.string.settings_password_protection_dialog_input_hint + ) + val dialog = DialogUtils.getDialog(this, dialogViewModel) + + dialogViewModel.showCancelButton { + dialog.dismiss() + } + + dialogViewModel.showOkButton( + { + Log.i( + "[Main Activity] Updating password for account [$identity] using auth info [$authInfo]" + ) + val newPassword = dialogViewModel.password + authInfo.password = newPassword + coreContext.core.addAuthInfo(authInfo) + + coreContext.core.refreshRegisters() + + dialog.dismiss() + }, + getString(R.string.dialog_authentication_required_change_password_label) + ) + + dialog.show() + } } diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageAttachmentData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageAttachmentData.kt index caa0a4f5c..fc66b68a5 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageAttachmentData.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageAttachmentData.kt @@ -35,10 +35,11 @@ class ChatMessageAttachmentData( init { val extension = FileUtils.getExtensionFromFileName(path) val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - isImage = FileUtils.isMimeImage(mime) - isVideo = FileUtils.isMimeVideo(mime) - isAudio = FileUtils.isMimeAudio(mime) - isPdf = FileUtils.isMimePdf(mime) + val mimeType = FileUtils.getMimeType(mime) + isImage = mimeType == FileUtils.MimeType.Image + isVideo = mimeType == FileUtils.MimeType.Video + isAudio = mimeType == FileUtils.MimeType.Audio + isPdf = mimeType == FileUtils.MimeType.Pdf } fun delete() { diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt index 03c9eacd7..f79851720 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt @@ -83,7 +83,6 @@ class ChatMessageContentData( val conferenceDate = MutableLiveData() val conferenceTime = MutableLiveData() val conferenceDuration = MutableLiveData() - var conferenceAddress = MutableLiveData() val showDuration = MutableLiveData() val isAlone: Boolean @@ -107,6 +106,8 @@ class ChatMessageContentData( stopVoiceRecording() } + private var conferenceAddress: String? = null + private fun getContent(): Content { return chatMessage.contents[contentIndex] } @@ -182,7 +183,7 @@ class ChatMessageContentData( val content = getContent() val filePath = content.filePath if (content.isFileTransfer) { - if (filePath == null || filePath.isEmpty()) { + if (filePath.isNullOrEmpty()) { val contentName = content.name if (contentName != null) { val file = FileUtils.getFileStoragePath(contentName) @@ -274,18 +275,26 @@ class ChatMessageContentData( filePath.value = path val extension = FileUtils.getExtensionFromFileName(path) val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - isImage.value = FileUtils.isMimeImage(mime) - isVideo.value = FileUtils.isMimeVideo(mime) && !isVoiceRecord - isAudio.value = FileUtils.isMimeAudio(mime) && !isVoiceRecord - isPdf.value = FileUtils.isMimePdf(mime) - val type = when { - isImage.value == true -> "image" - isVideo.value == true -> "video" - isAudio.value == true -> "audio" - isPdf.value == true -> "pdf" - isVoiceRecord -> "voice recording" - isConferenceIcs -> "conference invitation" - else -> "unknown" + val type = when (FileUtils.getMimeType(mime)) { + FileUtils.MimeType.Image -> { + isImage.value = true + "image" + } + FileUtils.MimeType.Video -> { + isVideo.value = !isVoiceRecord + if (isVoiceRecord) "voice recording" else "video" + } + FileUtils.MimeType.Audio -> { + isAudio.value = !isVoiceRecord + if (isVoiceRecord) "voice recording" else "audio" + } + FileUtils.MimeType.Pdf -> { + isPdf.value = true + "pdf" + } + else -> { + if (isConferenceIcs) "conference invitation" else "unknown" + } } Log.i( "[Content] Extension for file [$path] is [$extension], deduced type from MIME is [$type]" @@ -310,23 +319,26 @@ class ChatMessageContentData( Log.w( "[Content] Found ${if (content.isFile) "file" else "file transfer"} content with empty path..." ) - isImage.value = false - isVideo.value = false - isAudio.value = false - isPdf.value = false - isVoiceRecording.value = false - isConferenceSchedule.value = false } } else if (content.isFileTransfer) { downloadable.value = true val extension = FileUtils.getExtensionFromFileName(fileName.value!!) val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - isImage.value = FileUtils.isMimeImage(mime) - isVideo.value = FileUtils.isMimeVideo(mime) - isAudio.value = FileUtils.isMimeAudio(mime) - isPdf.value = FileUtils.isMimePdf(mime) - isVoiceRecording.value = false - isConferenceSchedule.value = false + when (FileUtils.getMimeType(mime)) { + FileUtils.MimeType.Image -> { + isImage.value = true + } + FileUtils.MimeType.Video -> { + isVideo.value = true + } + FileUtils.MimeType.Audio -> { + isAudio.value = true + } + FileUtils.MimeType.Pdf -> { + isPdf.value = true + } + else -> {} + } } else if (content.isIcalendar) { Log.i("[Content] Found content with icalendar body") isConferenceSchedule.value = true @@ -342,9 +354,9 @@ class ChatMessageContentData( val conferenceInfo = Factory.instance().createConferenceInfoFromIcalendarContent(content) val conferenceUri = conferenceInfo?.uri?.asStringUriOnly() if (conferenceInfo != null && conferenceUri != null) { - conferenceAddress.value = conferenceUri!! + conferenceAddress = conferenceUri!! Log.i( - "[Content] Created conference info from ICS with address ${conferenceAddress.value}" + "[Content] Created conference info from ICS with address $conferenceAddress" ) conferenceSubject.value = conferenceInfo.subject conferenceDescription.value = conferenceInfo.description @@ -407,7 +419,7 @@ class ChatMessageContentData( } fun callConferenceAddress() { - val address = conferenceAddress.value + val address = conferenceAddress if (address == null) { Log.e("[Content] Can't call null conference address!") return diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt index 2520d288e..dd35e964d 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt @@ -120,6 +120,17 @@ class DetailChatRoomFragment : MasterFragment - if (keyboardVisible && chatSendingViewModel.isEmojiPickerOpen.value == true) { - Log.d( - "[Chat Room] Emoji picker is opened, closing it because keyboard is now visible" - ) - chatSendingViewModel.isEmojiPickerOpen.value = false - } - } - Compatibility.setLocusIdInContentCaptureSession(binding.root, chatRoom) isSecure = chatRoom.currentParams.isEncryptionEnabled @@ -469,7 +471,7 @@ class DetailChatRoomFragment : MasterFragment navigateToImageFileViewer( + when (FileUtils.getMimeType(mime)) { + FileUtils.MimeType.Image -> navigateToImageFileViewer( preventScreenshots ) - FileUtils.isMimeVideo(mime) -> navigateToVideoFileViewer( + FileUtils.MimeType.Video -> navigateToVideoFileViewer( preventScreenshots ) - FileUtils.isMimeAudio(mime) -> navigateToAudioFileViewer( + FileUtils.MimeType.Audio -> navigateToAudioFileViewer( preventScreenshots ) - FileUtils.isMimePdf(mime) -> navigateToPdfFileViewer( + FileUtils.MimeType.Pdf -> navigateToPdfFileViewer( preventScreenshots ) - FileUtils.isMimePlainText(mime) -> navigateToTextFileViewer( + FileUtils.MimeType.PlainText -> navigateToTextFileViewer( preventScreenshots ) else -> { @@ -584,9 +586,9 @@ class DetailChatRoomFragment : MasterFragment - var index = 0 - var retryCount = 0 - var expectedChildCount = 0 + var index: Int + var loadSteps = 0 + var expectedChildCount: Int do { val events = listViewModel.events.value.orEmpty() expectedChildCount = events.size @@ -600,18 +602,17 @@ class DetailChatRoomFragment : MasterFragment + binding.setVoiceRecordingTouchListener { _, event -> if (corePreferences.holdToRecordVoiceMessage) { when (event.action) { MotionEvent.ACTION_DOWN -> { @@ -846,7 +847,7 @@ class DetailChatRoomFragment : MasterFragment() + val isFileTransferAvailable = MutableLiveData() + val requestKeyboardHidingEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -146,6 +148,7 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel() sendMessageEnabled.value = false isEmojiPickerOpen.value = false isEmojiPickerVisible.value = corePreferences.showEmojiPickerButton + isFileTransferAvailable.value = LinphoneUtils.isFileTransferAvailable() updateChatRoomReadOnlyState() } @@ -174,6 +177,10 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel() fun onTextToSendChanged(value: String) { sendMessageEnabled.value = value.trim().isNotEmpty() || attachments.value?.isNotEmpty() == true || isPendingVoiceRecord.value == true + + val showEmojiPicker = value.isEmpty() || AppUtils.isTextOnlyContainingEmoji(value) + isEmojiPickerVisible.value = corePreferences.showEmojiPickerButton && showEmojiPicker + if (value.isNotEmpty()) { if (attachFileEnabled.value == true && !corePreferences.allowMultipleFilesAndTextInSameMessage) { attachFileEnabled.value = false diff --git a/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt b/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt index a0b114149..fe08e43a0 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt @@ -25,6 +25,8 @@ import android.text.method.LinkMovementMethod import android.util.AttributeSet import androidx.appcompat.widget.AppCompatTextView import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.round /** * The purpose of this class is to have a TextView declared with wrap_content as width that won't @@ -52,10 +54,12 @@ class MultiLineWrapContentWidthTextView : AppCompatTextView { if (layout != null && layout.lineCount >= 2) { val maxLineWidth = ceil(getMaxLineWidth(layout)).toInt() - val uselessPaddingWidth = layout.width - maxLineWidth - val width = measuredWidth - uselessPaddingWidth - val height = measuredHeight - setMeasuredDimension(width, height) + if (maxLineWidth < measuredWidth) { + super.onMeasure( + MeasureSpec.makeMeasureSpec(maxLineWidth, MeasureSpec.getMode(widthSpec)), + heightSpec + ) + } } } @@ -63,10 +67,8 @@ class MultiLineWrapContentWidthTextView : AppCompatTextView { var maxWidth = 0.0f val lines = layout.lineCount for (i in 0 until lines) { - if (layout.getLineWidth(i) > maxWidth) { - maxWidth = layout.getLineWidth(i) - } + maxWidth = max(maxWidth, layout.getLineWidth(i)) } - return maxWidth + return round(maxWidth) } } diff --git a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceSchedulingViewModel.kt b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceSchedulingViewModel.kt index 04158e19e..47744863a 100644 --- a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceSchedulingViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceSchedulingViewModel.kt @@ -89,13 +89,26 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() { address.value = conferenceAddress!! - if (scheduleForLater.value == true && sendInviteViaChat.value == true) { - // Send conference info even when conf is not scheduled for later - // as the conference server doesn't invite participants automatically - val chatRoomParams = LinphoneUtils.getConferenceInvitationsChatRoomParams() - conferenceScheduler.sendInvitations(chatRoomParams) + if (scheduleForLater.value == true) { + if (sendInviteViaChat.value == true) { + // Send conference info even when conf is not scheduled for later + // as the conference server doesn't invite participants automatically + Log.i( + "[Conference Creation] Scheduled conference is ready, sending invitations by chat" + ) + val chatRoomParams = LinphoneUtils.getConferenceInvitationsChatRoomParams() + conferenceScheduler.sendInvitations(chatRoomParams) + } else { + Log.i( + "[Conference Creation] Scheduled conference is ready, we were asked not to send invitations by chat so leaving fragment" + ) + conferenceCreationInProgress.value = false + conferenceCreationCompletedEvent.value = Event(true) + } } else { - // Will be done in coreListener + Log.i("[Conference Creation] Group call is ready, leaving fragment") + conferenceCreationInProgress.value = false + conferenceCreationCompletedEvent.value = Event(true) } } else if (state == ConferenceScheduler.State.Error) { Log.e("[Conference Creation] Failed to create conference!") @@ -134,30 +147,6 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() { } } - private val coreListener: CoreListenerStub = object : CoreListenerStub() { - override fun onCallStateChanged( - core: Core, - call: Call, - state: Call.State?, - message: String - ) { - when (state) { - Call.State.OutgoingProgress -> { - conferenceCreationInProgress.value = false - } - Call.State.End -> { - Log.i("[Conference Creation] Call has ended, leaving waiting room fragment") - conferenceCreationCompletedEvent.value = Event(true) - } - Call.State.Error -> { - Log.w("[Conference Creation] Call has failed, leaving waiting room fragment") - conferenceCreationCompletedEvent.value = Event(true) - } - else -> {} - } - } - } - init { sipContactsSelected.value = true @@ -191,11 +180,9 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() { } conferenceScheduler.addListener(listener) - coreContext.core.addListener(coreListener) } override fun onCleared() { - coreContext.core.removeListener(coreListener) conferenceScheduler.removeListener(listener) participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy) diff --git a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceWaitingRoomViewModel.kt b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceWaitingRoomViewModel.kt index 72562512f..9238283ec 100644 --- a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceWaitingRoomViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceWaitingRoomViewModel.kt @@ -398,13 +398,15 @@ class ConferenceWaitingRoomViewModel : MessageNotifierViewModel() { private fun onAudioDevicesListUpdated() { val bluetoothDeviceAvailable = AudioRouteUtils.isBluetoothAudioRouteAvailable() + if (!bluetoothDeviceAvailable && audioRoutesEnabled.value == true) { + Log.w( + "[Conference Waiting Room] Bluetooth device no longer available, switching back to default microphone & earpiece/speaker" + ) + } audioRoutesEnabled.value = bluetoothDeviceAvailable if (!bluetoothDeviceAvailable) { audioRoutesSelected.value = false - Log.w( - "[Conference Waiting Room] Bluetooth device no longer available, switching back to default microphone & earpiece/speaker" - ) if (isBluetoothHeadsetSelected.value == true) { for (audioDevice in coreContext.core.audioDevices) { if (isVideoEnabled.value == true) { diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt index 94b0d81f9..31c5497ac 100644 --- a/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt @@ -20,6 +20,7 @@ package org.linphone.activities.main.contact.fragments import android.app.Activity +import android.app.Dialog import android.content.Intent import android.content.pm.PackageManager import android.os.Bundle @@ -38,9 +39,11 @@ import org.linphone.activities.main.MainActivity import org.linphone.activities.main.contact.data.ContactEditorData import org.linphone.activities.main.contact.data.NumberOrAddressEditorData import org.linphone.activities.main.contact.viewmodels.* +import org.linphone.activities.main.viewmodels.DialogViewModel import org.linphone.activities.navigateToContact import org.linphone.core.tools.Log import org.linphone.databinding.ContactEditorFragmentBinding +import org.linphone.utils.DialogUtils import org.linphone.utils.FileUtils import org.linphone.utils.PermissionHelper @@ -71,10 +74,38 @@ class ContactEditorFragment : GenericFragment(), S data.syncAccountName = null data.syncAccountType = null - if (data.friend == null && corePreferences.showNewContactAccountDialog) { - Log.i("[Contact Editor] New contact, ask user where to store it") - SyncAccountPickerFragment(this).show(childFragmentManager, "SyncAccountPicker") + if (data.friend == null) { + var atLeastASipAddressOrPhoneNumber = false + for (addr in data.addresses.value.orEmpty()) { + if (addr.newValue.value.orEmpty().isNotEmpty()) { + atLeastASipAddressOrPhoneNumber = true + break + } + } + if (!atLeastASipAddressOrPhoneNumber) { + for (number in data.numbers.value.orEmpty()) { + if (number.newValue.value.orEmpty().isNotEmpty()) { + atLeastASipAddressOrPhoneNumber = true + break + } + } + } + if (!atLeastASipAddressOrPhoneNumber) { + // Contact will be created without phone and SIP address + // Let's warn the user it won't be visible in Linphone app + Log.w( + "[Contact Editor] New contact without SIP address nor phone number, showing warning dialog" + ) + showInvisibleContactWarningDialog() + } else if (corePreferences.showNewContactAccountDialog) { + Log.i("[Contact Editor] New contact, ask user where to store it") + SyncAccountPickerFragment(this).show(childFragmentManager, "SyncAccountPicker") + } else { + Log.i("[Contact Editor] Saving new contact") + saveContact() + } } else { + Log.i("[Contact Editor] Saving contact changes") saveContact() } } @@ -98,7 +129,7 @@ class ContactEditorFragment : GenericFragment(), S } override fun onSyncAccountClicked(name: String?, type: String?) { - Log.i("[Contact Editor] Using account $name / $type") + Log.i("[Contact Editor] Saving new contact using account $name / $type") data.syncAccountName = name data.syncAccountType = type saveContact() @@ -146,6 +177,9 @@ class ContactEditorFragment : GenericFragment(), S Log.i("[Contact Editor] Displaying contact $savedContact") navigateToContact(id) } else { + Log.w( + "[Contact Editor] Can't display $savedContact because it doesn't have a refKey, going back" + ) goBack() } } @@ -183,4 +217,35 @@ class ContactEditorFragment : GenericFragment(), S startActivityForResult(chooserIntent, 0) } + + private fun showInvisibleContactWarningDialog() { + val dialogViewModel = + DialogViewModel(getString(R.string.contacts_new_contact_wont_be_visible_warning_dialog)) + val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel) + + dialogViewModel.showCancelButton( + { + Log.i("[Contact Editor] Aborting new contact saving") + dialog.dismiss() + }, + getString(R.string.no) + ) + + dialogViewModel.showOkButton( + { + dialog.dismiss() + + if (corePreferences.showNewContactAccountDialog) { + Log.i("[Contact Editor] New contact, ask user where to store it") + SyncAccountPickerFragment(this).show(childFragmentManager, "SyncAccountPicker") + } else { + Log.i("[Contact Editor] Saving new contact") + saveContact() + } + }, + getString(R.string.yes) + ) + + dialog.show() + } } diff --git a/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactViewModel.kt b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactViewModel.kt index 8d9fb8ec3..ac607fa73 100644 --- a/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactViewModel.kt @@ -34,7 +34,7 @@ import org.linphone.activities.main.contact.data.ContactNumberOrAddressData import org.linphone.activities.main.viewmodels.MessageNotifierViewModel import org.linphone.contact.ContactDataInterface import org.linphone.contact.ContactsUpdatedListenerStub -import org.linphone.contact.hasPresence +import org.linphone.contact.hasLongTermPresence import org.linphone.core.* import org.linphone.core.tools.Log import org.linphone.utils.Event @@ -81,6 +81,8 @@ class ContactViewModel(friend: Friend, async: Boolean = false) : MessageNotifier val readOnlyNativeAddressBook = MutableLiveData() + val hasLongTermPresence = MutableLiveData() + private val chatRoomListener = object : ChatRoomListenerStub() { override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) { if (state == ChatRoom.State.Created) { @@ -147,16 +149,19 @@ class ContactViewModel(friend: Friend, async: Boolean = false) : MessageNotifier isNativeContact.postValue(friend.refKey != null) presenceStatus.postValue(friend.consolidatedPresence) readOnlyNativeAddressBook.postValue(corePreferences.readOnlyNativeContacts) + hasLongTermPresence.postValue(friend.hasLongTermPresence()) } else { contact.value = friend displayName.value = friend.name isNativeContact.value = friend.refKey != null presenceStatus.value = friend.consolidatedPresence readOnlyNativeAddressBook.value = corePreferences.readOnlyNativeContacts + hasLongTermPresence.value = friend.hasLongTermPresence() } friend.addListener { presenceStatus.value = it.consolidatedPresence + hasLongTermPresence.value = it.hasLongTermPresence() } } @@ -269,8 +274,4 @@ class ContactViewModel(friend: Friend, async: Boolean = false) : MessageNotifier } numbersAndAddresses.postValue(list) } - - fun hasPresence(): Boolean { - return contact.value?.hasPresence() ?: false - } } diff --git a/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactsListViewModel.kt b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactsListViewModel.kt index e71ee6bde..becfd4ad3 100644 --- a/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactsListViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactsListViewModel.kt @@ -119,17 +119,17 @@ class ContactsListViewModel : ViewModel() { previousFilter = filterValue val domain = if (sipContactsSelected.value == true) coreContext.core.defaultAccount?.params?.domain ?: "" else "" - val filter = MagicSearch.Source.Friends.toInt() or MagicSearch.Source.LdapServers.toInt() + val sources = MagicSearch.Source.Friends.toInt() or MagicSearch.Source.LdapServers.toInt() val aggregation = MagicSearch.Aggregation.Friend searchResultsPending = true fastFetchJob?.cancel() Log.i( - "[Contacts] Asking Magic search for contacts matching filter [$filterValue], domain [$domain] and in sources [$filter]" + "[Contacts] Asking Magic search for contacts matching filter [$filterValue], domain [$domain] and in sources [$sources]" ) coreContext.contactsManager.magicSearch.getContactsListAsync( filterValue, domain, - filter, + sources, aggregation ) diff --git a/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt b/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt index b59729e39..926442817 100644 --- a/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt @@ -273,6 +273,24 @@ class DialerFragment : SecureFragment() { // Don't check the following the previous permissions are being asked checkTelecomManagerPermissions() } + + // See https://developer.android.com/about/versions/14/behavior-changes-14#fgs-types + if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) { + val fullScreenIntentPermission = Compatibility.hasFullScreenIntentPermission( + requireContext() + ) + Log.i( + "[Dialer] Android 14 or above detected: full-screen intent permission is ${if (fullScreenIntentPermission) "granted" else "not granted"}" + ) + if (!fullScreenIntentPermission) { + (requireActivity() as MainActivity).showSnackBar( + R.string.android_14_full_screen_intent_permission_not_granted, + R.string.android_14_go_to_full_screen_intent_permission_setting + ) { + Compatibility.requestFullScreenIntentPermission(requireContext()) + } + } + } } @TargetApi(Version.API26_O_80) diff --git a/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt b/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt index 4f3997043..b931023d3 100644 --- a/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt @@ -134,7 +134,7 @@ class DialerViewModel : LogsUploadViewModel() { ) { if (result == VersionUpdateCheckResult.NewVersionAvailable) { Log.i("[Dialer] Update available, version [$version], url [$url]") - if (url != null && url.isNotEmpty()) { + if (!url.isNullOrEmpty()) { updateAvailableEvent.value = Event(url) } } diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/PdfViewerFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/PdfViewerFragment.kt index 799543fc4..2e123d104 100644 --- a/app/src/main/java/org/linphone/activities/main/files/fragments/PdfViewerFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/files/fragments/PdfViewerFragment.kt @@ -54,7 +54,11 @@ class PdfViewerFragment : GenericViewerFragment() )[PdfFileViewModel::class.java] binding.viewModel = viewModel - adapter = PdfPagesListAdapter(viewModel) - binding.pdfViewPager.adapter = adapter + viewModel.rendererReady.observe(viewLifecycleOwner) { + it.consume { + adapter = PdfPagesListAdapter(viewModel) + binding.pdfViewPager.adapter = adapter + } + } } } diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/TopBarFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/TopBarFragment.kt index d636d194c..e956f4654 100644 --- a/app/src/main/java/org/linphone/activities/main/files/fragments/TopBarFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/files/fragments/TopBarFragment.kt @@ -92,8 +92,8 @@ class TopBarFragment : GenericFragment() { val extension = FileUtils.getExtensionFromFileName(filePath) val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - when { - FileUtils.isMimeImage(mime) -> { + when (FileUtils.getMimeType(mime)) { + FileUtils.MimeType.Image -> { val export = lifecycleScope.async { Compatibility.addImageToMediaStore(requireContext(), content) } @@ -108,7 +108,7 @@ class TopBarFragment : GenericFragment() { ) } } - FileUtils.isMimeVideo(mime) -> { + FileUtils.MimeType.Video -> { val export = lifecycleScope.async { Compatibility.addVideoToMediaStore(requireContext(), content) } @@ -123,7 +123,7 @@ class TopBarFragment : GenericFragment() { ) } } - FileUtils.isMimeAudio(mime) -> { + FileUtils.MimeType.Audio -> { val export = lifecycleScope.async { Compatibility.addAudioToMediaStore(requireContext(), content) } diff --git a/app/src/main/java/org/linphone/activities/main/files/viewmodels/AudioFileViewModel.kt b/app/src/main/java/org/linphone/activities/main/files/viewmodels/AudioFileViewModel.kt index a196bd76e..33e83b7d9 100644 --- a/app/src/main/java/org/linphone/activities/main/files/viewmodels/AudioFileViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/files/viewmodels/AudioFileViewModel.kt @@ -72,7 +72,7 @@ class AudioFileViewModel(content: Content) : FileViewerViewModel(content), Media override fun getCurrentPosition(): Int { try { return mediaPlayer.currentPosition - } catch (ise: IllegalStateException) {} + } catch (_: IllegalStateException) {} return 0 } diff --git a/app/src/main/java/org/linphone/activities/main/files/viewmodels/PdfFileViewModel.kt b/app/src/main/java/org/linphone/activities/main/files/viewmodels/PdfFileViewModel.kt index 34df29d60..259957fac 100644 --- a/app/src/main/java/org/linphone/activities/main/files/viewmodels/PdfFileViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/files/viewmodels/PdfFileViewModel.kt @@ -26,10 +26,15 @@ import android.widget.ImageView import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope import java.io.File +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.core.Content import org.linphone.core.tools.Log +import org.linphone.utils.Event class PdfFileViewModelFactory(private val content: Content) : ViewModelProvider.NewInstanceFactory() { @@ -43,45 +48,67 @@ class PdfFileViewModelFactory(private val content: Content) : class PdfFileViewModel(content: Content) : FileViewerViewModel(content) { val operationInProgress = MutableLiveData() - private val pdfRenderer: PdfRenderer + val rendererReady = MutableLiveData>() + + private lateinit var pdfRenderer: PdfRenderer init { operationInProgress.value = false - val input = ParcelFileDescriptor.open(File(filePath), ParcelFileDescriptor.MODE_READ_ONLY) - pdfRenderer = PdfRenderer(input) - Log.i("[PDF Viewer] ${pdfRenderer.pageCount} pages in file $filePath") + viewModelScope.launch { + withContext(Dispatchers.IO) { + val input = ParcelFileDescriptor.open( + File(filePath), + ParcelFileDescriptor.MODE_READ_ONLY + ) + pdfRenderer = PdfRenderer(input) + Log.i("[PDF Viewer] ${pdfRenderer.pageCount} pages in file $filePath") + rendererReady.postValue(Event(true)) + } + } } override fun onCleared() { - pdfRenderer.close() + if (this::pdfRenderer.isInitialized) { + pdfRenderer.close() + } super.onCleared() } fun getPagesCount(): Int { - return pdfRenderer.pageCount + if (this::pdfRenderer.isInitialized) { + return pdfRenderer.pageCount + } + return 0 } fun loadPdfPageInto(index: Int, view: ImageView) { - try { - operationInProgress.value = true + viewModelScope.launch { + withContext(Dispatchers.IO) { + try { + operationInProgress.postValue(true) - val page: PdfRenderer.Page = pdfRenderer.openPage(index) - val width = if (coreContext.screenWidth <= coreContext.screenHeight) coreContext.screenWidth else coreContext.screenHeight - val bm = Bitmap.createBitmap( - width.toInt(), - (width / page.width * page.height).toInt(), - Bitmap.Config.ARGB_8888 - ) - page.render(bm, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) - page.close() + val page: PdfRenderer.Page = pdfRenderer.openPage(index) + val width = + if (coreContext.screenWidth <= coreContext.screenHeight) coreContext.screenWidth else coreContext.screenHeight + val bm = Bitmap.createBitmap( + width.toInt(), + (width / page.width * page.height).toInt(), + Bitmap.Config.ARGB_8888 + ) + page.render(bm, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) + page.close() - view.setImageBitmap(bm) + withContext(Dispatchers.Main) { + view.setImageBitmap(bm) + } - operationInProgress.value = false - } catch (e: Exception) { - Log.e("[PDF Viewer] Exception: $e") - operationInProgress.value = false + operationInProgress.postValue(false) + } catch (e: Exception) { + Log.e("[PDF Viewer] Exception: $e") + operationInProgress.postValue(false) + } + } } } } diff --git a/app/src/main/java/org/linphone/activities/main/files/viewmodels/TextFileViewModel.kt b/app/src/main/java/org/linphone/activities/main/files/viewmodels/TextFileViewModel.kt index fd79d5f6f..56aa44dbc 100644 --- a/app/src/main/java/org/linphone/activities/main/files/viewmodels/TextFileViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/files/viewmodels/TextFileViewModel.kt @@ -47,17 +47,10 @@ class TextFileViewModel(content: Content) : FileViewerViewModel(content) { val text = MutableLiveData() init { - operationInProgress.value = false - - openFile() - } - - private fun openFile() { - operationInProgress.value = true - viewModelScope.launch { withContext(Dispatchers.IO) { try { + operationInProgress.postValue(true) val br = BufferedReader(FileReader(filePath)) var line: String? val textBuilder = StringBuilder() diff --git a/app/src/main/java/org/linphone/activities/main/fragments/SecureFragment.kt b/app/src/main/java/org/linphone/activities/main/fragments/SecureFragment.kt index bb84d2359..7cc660755 100644 --- a/app/src/main/java/org/linphone/activities/main/fragments/SecureFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/fragments/SecureFragment.kt @@ -50,7 +50,7 @@ abstract class SecureFragment : GenericFragment() { override fun onResume() { if (isSecure) { - enableSecureMode(isSecure) + enableSecureMode(true) } else { // This is a workaround to prevent a small blink showing the previous secured screen lifecycleScope.launch { diff --git a/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt index ee2e24d1b..8bb760ec9 100644 --- a/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt @@ -53,7 +53,9 @@ class DetailCallLogFragment : GenericFragment() { viewModel = callLogGroup.lastCallLogViewModel binding.viewModel = viewModel - viewModel.addRelatedCallLogs(callLogGroup.callLogs) + if (viewModel.relatedCallLogs.value.orEmpty().isEmpty()) { + viewModel.addRelatedCallLogs(callLogGroup.callLogs) + } useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false diff --git a/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt b/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt index 75da8a63f..a5d1b74cb 100644 --- a/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt @@ -44,7 +44,7 @@ class CallLogViewModel(val callLog: CallLog, private val isRelated: Boolean = fa val statusIconResource: Int by lazy { if (callLog.dir == Call.Dir.Incoming) { - if (callLog.status == Call.Status.Missed) { + if (LinphoneUtils.isCallLogMissed(callLog)) { R.drawable.call_status_missed } else { R.drawable.call_status_incoming @@ -56,7 +56,7 @@ class CallLogViewModel(val callLog: CallLog, private val isRelated: Boolean = fa val iconContentDescription: Int by lazy { if (callLog.dir == Call.Dir.Incoming) { - if (callLog.status == Call.Status.Missed) { + if (LinphoneUtils.isCallLogMissed(callLog)) { R.string.content_description_missed_call } else { R.string.content_description_incoming_call @@ -68,7 +68,7 @@ class CallLogViewModel(val callLog: CallLog, private val isRelated: Boolean = fa val directionIconResource: Int by lazy { if (callLog.dir == Call.Dir.Incoming) { - if (callLog.status == Call.Status.Missed) { + if (LinphoneUtils.isCallLogMissed(callLog)) { R.drawable.call_missed } else { R.drawable.call_incoming diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt index 049bdd531..b06648585 100644 --- a/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt @@ -98,14 +98,6 @@ class AccountSettingsFragment : GenericSettingFragment>() } - val publishPresenceToggledEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - val displayUsernameInsteadOfIdentity = corePreferences.replaceSipUriByUsername private var accountToDelete: Account? = null @@ -293,7 +289,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel( account.params = params transportIndex.value = account.params.transport.toInt() } else { - Log.e("[Account Settings] Couldn't parse address: $address") + Log.e("[Account Settings] Couldn't parse address: $newValue") } } } @@ -439,16 +435,6 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel( } val limeServerUrl = MutableLiveData() - val publishPresenceListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - val params = account.params.clone() - params.isPublishEnabled = newValue - account.params = params - publishPresenceToggledEvent.value = Event(true) - } - } - val publishPresence = MutableLiveData() - val disableBundleModeListener = object : SettingListenerStub() { override fun onBoolValueChanged(newValue: Boolean) { val params = account.params.clone() @@ -518,7 +504,6 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel( limeServerUrl.value = params.limeServerUrl hideLinkPhoneNumber.value = corePreferences.hideLinkPhoneNumber || params.identityAddress?.domain != corePreferences.defaultDomain - publishPresence.value = params.isPublishEnabled disableBundleMode.value = !params.isRtpBundleEnabled } diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AudioSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AudioSettingsViewModel.kt index 192106b83..9e7bc11ce 100644 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AudioSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AudioSettingsViewModel.kt @@ -141,7 +141,7 @@ class AudioSettingsViewModel : GenericSettingsViewModel() { override fun onTextValueChanged(newValue: String) { try { core.micGainDb = newValue.toFloat() - } catch (nfe: NumberFormatException) { + } catch (_: NumberFormatException) { } } } @@ -151,7 +151,7 @@ class AudioSettingsViewModel : GenericSettingsViewModel() { override fun onTextValueChanged(newValue: String) { try { core.playbackGainDb = newValue.toFloat() - } catch (nfe: NumberFormatException) { + } catch (_: NumberFormatException) { } } } diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt index 42eab7990..45fb318f1 100644 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt @@ -97,7 +97,7 @@ class CallSettingsViewModel : GenericSettingsViewModel() { Log.w("[Call Settings] Disabling Telecom Manager auto-enable") prefs.manuallyDisabledTelecomManager = true } - prefs.useTelecomManager = newValue + prefs.useTelecomManager = false } } } @@ -169,7 +169,7 @@ class CallSettingsViewModel : GenericSettingsViewModel() { override fun onTextValueChanged(newValue: String) { try { prefs.autoAnswerDelay = newValue.toInt() - } catch (nfe: NumberFormatException) { + } catch (_: NumberFormatException) { } } } @@ -179,7 +179,7 @@ class CallSettingsViewModel : GenericSettingsViewModel() { override fun onTextValueChanged(newValue: String) { try { core.incTimeout = newValue.toInt() - } catch (nfe: NumberFormatException) { + } catch (_: NumberFormatException) { } } } diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt index e7a563ad0..d926b7b45 100644 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt @@ -61,7 +61,7 @@ class ChatSettingsViewModel : GenericSettingsViewModel() { val maxSize = newValue.toInt() core.maxSizeForAutoDownloadIncomingFiles = maxSize updateAutoDownloadIndexFromMaxSize(maxSize) - } catch (nfe: NumberFormatException) { + } catch (_: NumberFormatException) { } } } diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ContactsSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ContactsSettingsViewModel.kt index 619e15849..68847e3fb 100644 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ContactsSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ContactsSettingsViewModel.kt @@ -22,6 +22,8 @@ package org.linphone.activities.main.settings.viewmodels import androidx.lifecycle.MutableLiveData import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.activities.main.settings.SettingListenerStub +import org.linphone.core.ConsolidatedPresence +import org.linphone.core.tools.Log import org.linphone.utils.Event import org.linphone.utils.PermissionHelper @@ -40,6 +42,30 @@ class ContactsSettingsViewModel : GenericSettingsViewModel() { val friendListSubscribe = MutableLiveData() val rlsAddressAvailable = MutableLiveData() + val publishPresenceListener = object : SettingListenerStub() { + override fun onBoolValueChanged(newValue: Boolean) { + prefs.publishPresence = newValue + + if (newValue) { + // Publish online presence when enabling setting + Log.i( + "[Contacts Settings] Presence has been enabled, PUBLISHING presence as Online" + ) + core.consolidatedPresence = ConsolidatedPresence.Online + } else { + // Unpublish presence when disabling setting + Log.i("[Contacts Settings] Presence has been disabled, un-PUBLISHING presence info") + core.consolidatedPresence = ConsolidatedPresence.Offline + } + + publishPresenceToggledEvent.value = Event(true) + } + } + val publishPresence = MutableLiveData() + val publishPresenceToggledEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + val showNewContactAccountDialogListener = object : SettingListenerStub() { override fun onBoolValueChanged(newValue: Boolean) { prefs.showNewContactAccountDialog = newValue @@ -51,12 +77,12 @@ class ContactsSettingsViewModel : GenericSettingsViewModel() { override fun onBoolValueChanged(newValue: Boolean) { if (newValue) { if (PermissionHelper.get().hasWriteContactsPermission()) { - prefs.storePresenceInNativeContact = newValue + prefs.storePresenceInNativeContact = true } else { askWriteContactsPermissionForPresenceStorageEvent.value = Event(true) } } else { - prefs.storePresenceInNativeContact = newValue + prefs.storePresenceInNativeContact = false } } } @@ -97,6 +123,8 @@ class ContactsSettingsViewModel : GenericSettingsViewModel() { friendListSubscribe.value = core.isFriendListSubscriptionEnabled rlsAddressAvailable.value = !core.config.getString("sip", "rls_uri", "").isNullOrEmpty() + publishPresence.value = prefs.publishPresence + showNewContactAccountDialog.value = prefs.showNewContactAccountDialog nativePresence.value = prefs.storePresenceInNativeContact showOrganization.value = prefs.displayOrganization diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/NetworkSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/NetworkSettingsViewModel.kt index c282b006e..057997971 100644 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/NetworkSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/NetworkSettingsViewModel.kt @@ -52,7 +52,7 @@ class NetworkSettingsViewModel : GenericSettingsViewModel() { try { val port = newValue.toInt() setTransportPort(port) - } catch (nfe: NumberFormatException) { + } catch (_: NumberFormatException) { } } } diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/TunnelSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/TunnelSettingsViewModel.kt index 3822fe5fa..8bdcf2110 100644 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/TunnelSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/TunnelSettingsViewModel.kt @@ -43,7 +43,7 @@ class TunnelSettingsViewModel : GenericSettingsViewModel() { val config = getTunnelConfig() config.port = newValue.toInt() updateTunnelConfig(config) - } catch (nfe: NumberFormatException) { + } catch (_: NumberFormatException) { } } } @@ -72,7 +72,7 @@ class TunnelSettingsViewModel : GenericSettingsViewModel() { val config = getTunnelConfig() config.port2 = newValue.toInt() updateTunnelConfig(config) - } catch (nfe: NumberFormatException) { + } catch (_: NumberFormatException) { } } } diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/VideoSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/VideoSettingsViewModel.kt index 68278a2da..db1db30ed 100644 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/VideoSettingsViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/VideoSettingsViewModel.kt @@ -87,7 +87,17 @@ class VideoSettingsViewModel : GenericSettingsViewModel() { val videoPresetListener = object : SettingListenerStub() { override fun onListValueChanged(position: Int) { videoPresetIndex.value = position // Needed to display/hide two below settings - core.videoPreset = videoPresetLabels.value.orEmpty()[position] + val currentPreset = core.videoPreset + val newPreset = videoPresetLabels.value.orEmpty()[position] + if (newPreset != currentPreset) { + if (currentPreset == "custom") { + // Not "custom" anymore, reset FPS & bandwidth + core.preferredFramerate = 0f + core.downloadBandwidth = 0 + core.uploadBandwidth = 0 + } + core.videoPreset = newPreset + } } } val videoPresetIndex = MutableLiveData() @@ -106,7 +116,7 @@ class VideoSettingsViewModel : GenericSettingsViewModel() { try { core.downloadBandwidth = newValue.toInt() core.uploadBandwidth = newValue.toInt() - } catch (nfe: NumberFormatException) { + } catch (_: NumberFormatException) { } } } diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/ListTopBarViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/ListTopBarViewModel.kt index c8980f75d..0c0ffc090 100644 --- a/app/src/main/java/org/linphone/activities/main/viewmodels/ListTopBarViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/viewmodels/ListTopBarViewModel.kt @@ -63,7 +63,7 @@ class ListTopBarViewModel : ViewModel() { val list = arrayListOf() selectedItems.value = list - isSelectionNotEmpty.value = list.isNotEmpty() + isSelectionNotEmpty.value = false } fun onToggleSelect(position: Int) { diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/CallsListFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/CallsListFragment.kt index 2cd8ba9a3..d48bf8e71 100644 --- a/app/src/main/java/org/linphone/activities/voip/fragments/CallsListFragment.kt +++ b/app/src/main/java/org/linphone/activities/voip/fragments/CallsListFragment.kt @@ -62,8 +62,6 @@ class CallsListFragment : GenericVideoPreviewFragment @@ -90,12 +87,13 @@ class ConferenceParticipantsFragment : GenericVideoPreviewFragment : GenericFragmen private var switchY: Float = 0f private var switchCameraImageView: ImageView? = null - private lateinit var videoPreviewTextureView: TextureView private val previewTouchListener = View.OnTouchListener { view, event -> when (event.action) { @@ -67,19 +66,13 @@ abstract class GenericVideoPreviewFragment : GenericFragmen } } - protected fun setupLocalViewPreview(localVideoPreview: TextureView, switchCamera: ImageView?) { - if (coreContext.core.currentCall?.currentParams?.isVideoEnabled == true) { - videoPreviewTextureView = localVideoPreview - switchCameraImageView = switchCamera - videoPreviewTextureView.setOnTouchListener(previewTouchListener) - } + protected fun setupLocalVideoPreview(localVideoPreview: TextureView, switchCamera: ImageView?) { + switchCameraImageView = switchCamera + localVideoPreview.setOnTouchListener(previewTouchListener) + coreContext.core.nativePreviewWindowId = localVideoPreview } - override fun onResume() { - super.onResume() - - if (::videoPreviewTextureView.isInitialized) { - coreContext.core.nativePreviewWindowId = videoPreviewTextureView - } + protected fun cleanUpLocalVideoPreview(localVideoPreview: TextureView) { + localVideoPreview.setOnTouchListener(null) } } diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/OutgoingCallFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/OutgoingCallFragment.kt index 32bc7b828..8ba1baa55 100644 --- a/app/src/main/java/org/linphone/activities/voip/fragments/OutgoingCallFragment.kt +++ b/app/src/main/java/org/linphone/activities/voip/fragments/OutgoingCallFragment.kt @@ -24,7 +24,6 @@ import android.os.SystemClock import android.view.View import android.widget.Chronometer import androidx.navigation.navGraphViewModels -import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R import org.linphone.activities.navigateToActiveCall import org.linphone.activities.voip.viewmodels.CallsViewModel @@ -46,8 +45,6 @@ class OutgoingCallFragment : GenericVideoPreviewFragment { + Call.State.OutgoingRinging, Call.State.OutgoingEarlyMedia -> { Log.i( "[Single Call] New current call is in [$callState] state, switching to OutgoingCall fragment" ) @@ -193,15 +191,20 @@ class SingleCallFragment : GenericVideoPreviewFragment() { private fun showZrtpDialog(call: Call) { if (zrtpDialog != null && zrtpDialog?.isShowing == true) { - Log.e("[Status Fragment] ZRTP dialog already visible") - return + Log.w( + "[Status Fragment] ZRTP dialog already visible, closing it and creating a new one" + ) + zrtpDialog?.dismiss() + zrtpDialog = null } val token = call.authenticationToken @@ -125,8 +128,19 @@ class StatusFragment : GenericFragment() { viewModel.showCancelButton( { - call.authenticationTokenVerified = false - this@StatusFragment.viewModel.updateEncryptionInfo(call) + if (call.state != Call.State.End && call.state != Call.State.Released) { + if (call.authenticationTokenVerified) { + Log.w( + "[Status Fragment] Removing trust from previously verified ZRTP SAS auth token" + ) + this@StatusFragment.viewModel.previouslyDeclineToken = true + call.authenticationTokenVerified = false + } + } else { + Log.e( + "[Status Fragment] Can't decline the ZRTP SAS token, call is in state [${call.state}]" + ) + } dialog.dismiss() zrtpDialog = null }, @@ -135,8 +149,13 @@ class StatusFragment : GenericFragment() { viewModel.showOkButton( { - call.authenticationTokenVerified = true - this@StatusFragment.viewModel.updateEncryptionInfo(call) + if (call.state != Call.State.End && call.state != Call.State.Released) { + call.authenticationTokenVerified = true + } else { + Log.e( + "[Status Fragment] Can't verify the ZRTP SAS token, call is in state [${call.state}]" + ) + } dialog.dismiss() zrtpDialog = null }, diff --git a/app/src/main/java/org/linphone/activities/voip/viewmodels/ControlsViewModel.kt b/app/src/main/java/org/linphone/activities/voip/viewmodels/ControlsViewModel.kt index 13b0f999c..9b275658f 100644 --- a/app/src/main/java/org/linphone/activities/voip/viewmodels/ControlsViewModel.kt +++ b/app/src/main/java/org/linphone/activities/voip/viewmodels/ControlsViewModel.kt @@ -81,6 +81,8 @@ class ControlsViewModel : ViewModel() { val attendedTransfer = MutableLiveData() + val chatDisabled = MutableLiveData() + val goToConferenceParticipantsListEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -206,6 +208,7 @@ class ControlsViewModel : ViewModel() { init { coreContext.core.addListener(listener) + chatDisabled.value = corePreferences.disableChat fullScreenMode.value = false extraButtonsMenuTranslateY.value = AppUtils.getDimension( R.dimen.voip_call_extra_buttons_translate_y @@ -536,7 +539,7 @@ class ControlsViewModel : ViewModel() { isVideoEnabled.value = enabled showTakeSnapshotButton.value = enabled && corePreferences.showScreenshotButton - var isVideoBeingSent = if (coreContext.core.currentCall?.conference != null) { + val isVideoBeingSent = if (coreContext.core.currentCall?.conference != null) { val videoDirection = coreContext.core.currentCall?.currentParams?.videoDirection videoDirection == MediaDirection.SendRecv || videoDirection == MediaDirection.SendOnly } else { diff --git a/app/src/main/java/org/linphone/activities/voip/viewmodels/StatusViewModel.kt b/app/src/main/java/org/linphone/activities/voip/viewmodels/StatusViewModel.kt index ab8d3b597..ec2d25fbd 100644 --- a/app/src/main/java/org/linphone/activities/voip/viewmodels/StatusViewModel.kt +++ b/app/src/main/java/org/linphone/activities/voip/viewmodels/StatusViewModel.kt @@ -42,6 +42,8 @@ class StatusViewModel : StatusViewModel() { MutableLiveData>() } + var previouslyDeclineToken = false + private val listener = object : CoreListenerStub() { override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) { updateCallQualityIcon() @@ -54,8 +56,11 @@ class StatusViewModel : StatusViewModel() { authenticationToken: String? ) { updateEncryptionInfo(call) - if (call.currentParams.mediaEncryption == MediaEncryption.ZRTP && !call.authenticationTokenVerified && call.authenticationToken != null) { - showZrtpDialogEvent.value = Event(call) + // Check if we just declined a previously validated token + // In that case, don't show the ZRTP dialog again + if (!previouslyDeclineToken) { + previouslyDeclineToken = false + showZrtpDialog(call) } } @@ -79,10 +84,7 @@ class StatusViewModel : StatusViewModel() { val currentCall = coreContext.core.currentCall if (currentCall != null) { updateEncryptionInfo(currentCall) - - if (currentCall.currentParams.mediaEncryption == MediaEncryption.ZRTP && !currentCall.authenticationTokenVerified && currentCall.authenticationToken != null) { - showZrtpDialogEvent.value = Event(currentCall) - } + showZrtpDialog(currentCall) } } @@ -94,8 +96,8 @@ class StatusViewModel : StatusViewModel() { fun showZrtpDialog() { val currentCall = coreContext.core.currentCall - if (currentCall?.authenticationToken != null && currentCall.currentParams.mediaEncryption == MediaEncryption.ZRTP) { - showZrtpDialogEvent.value = Event(currentCall) + if (currentCall != null) { + showZrtpDialog(currentCall, force = true) } } @@ -112,6 +114,9 @@ class StatusViewModel : StatusViewModel() { encryptionContentDescription.value = R.string.content_description_call_secured return } + if (call.state == Call.State.End || call.state == Call.State.Released) { + return + } when (call.currentParams.mediaEncryption ?: MediaEncryption.None) { MediaEncryption.SRTP, MediaEncryption.DTLS -> { @@ -139,6 +144,16 @@ class StatusViewModel : StatusViewModel() { } } + private fun showZrtpDialog(call: Call, force: Boolean = false) { + if ( + call.currentParams.mediaEncryption == MediaEncryption.ZRTP && + call.authenticationToken != null && + (!call.authenticationTokenVerified || force) + ) { + showZrtpDialogEvent.value = Event(call) + } + } + private fun updateCallQualityIcon() { val call = coreContext.core.currentCall ?: coreContext.core.calls.firstOrNull() val quality = call?.currentQuality ?: 0f diff --git a/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt new file mode 100644 index 000000000..cccd52d54 --- /dev/null +++ b/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt @@ -0,0 +1,32 @@ +/* + * 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.compatibility + +import android.annotation.TargetApi +import android.content.ClipboardManager + +@TargetApi(28) +class Api28Compatibility { + companion object { + fun clearClipboard(clipboard: ClipboardManager) { + clipboard.clearPrimaryClip() + } + } +} diff --git a/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt index f225df602..1afb8cfbf 100644 --- a/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt @@ -262,6 +262,10 @@ class Api31Compatibility { context.startForegroundService(intent) } catch (fssnae: ForegroundServiceStartNotAllowedException) { Log.e("[Api31 Compatibility] Can't start service as foreground! $fssnae") + } catch (se: SecurityException) { + Log.e("[Api31 Compatibility] Can't start service as foreground! $se") + } catch (e: Exception) { + Log.e("[Api31 Compatibility] Can't start service as foreground! $e") } } @@ -270,6 +274,10 @@ class Api31Compatibility { service.startForeground(notifId, notif) } catch (fssnae: ForegroundServiceStartNotAllowedException) { Log.e("[Api31 Compatibility] Can't start service as foreground! $fssnae") + } catch (se: SecurityException) { + Log.e("[Api31 Compatibility] Can't start service as foreground! $se") + } catch (e: Exception) { + Log.e("[Api31 Compatibility] Can't start service as foreground! $e") } } diff --git a/app/src/main/java/org/linphone/compatibility/Api34Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api34Compatibility.kt new file mode 100644 index 000000000..66f98c277 --- /dev/null +++ b/app/src/main/java/org/linphone/compatibility/Api34Compatibility.kt @@ -0,0 +1,85 @@ +/* + * 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.compatibility + +import android.annotation.TargetApi +import android.app.ForegroundServiceStartNotAllowedException +import android.app.Notification +import android.app.NotificationManager +import android.app.Service +import android.content.Context +import android.content.Intent +import android.content.pm.ServiceInfo +import android.net.Uri +import android.provider.Settings +import androidx.core.content.ContextCompat +import org.linphone.core.tools.Log + +@TargetApi(34) +class Api34Compatibility { + companion object { + fun hasFullScreenIntentPermission(context: Context): Boolean { + val notificationManager = context.getSystemService(NotificationManager::class.java) as NotificationManager + // See https://developer.android.com/reference/android/app/NotificationManager#canUseFullScreenIntent%28%29 + return notificationManager.canUseFullScreenIntent() + } + + fun requestFullScreenIntentPermission(context: Context) { + val intent = Intent() + // See https://developer.android.com/reference/android/provider/Settings#ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT + intent.action = Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT + intent.data = Uri.parse("package:${context.packageName}") + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) + ContextCompat.startActivity(context, intent, null) + } + + fun startCallForegroundService(service: Service, notifId: Int, notif: Notification) { + try { + service.startForeground( + notifId, + notif, + ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA + ) + } catch (fssnae: ForegroundServiceStartNotAllowedException) { + Log.e("[Api34 Compatibility] Can't start service as foreground! $fssnae") + } catch (se: SecurityException) { + Log.e("[Api34 Compatibility] Can't start service as foreground! $se") + } catch (e: Exception) { + Log.e("[Api34 Compatibility] Can't start service as foreground! $e") + } + } + + fun startDataSyncForegroundService(service: Service, notifId: Int, notif: Notification) { + try { + service.startForeground( + notifId, + notif, + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + ) + } catch (fssnae: ForegroundServiceStartNotAllowedException) { + Log.e("[Api34 Compatibility] Can't start service as foreground! $fssnae") + } catch (se: SecurityException) { + Log.e("[Api34 Compatibility] Can't start service as foreground! $se") + } catch (e: Exception) { + Log.e("[Api34 Compatibility] Can't start service as foreground! $e") + } + } + } +} diff --git a/app/src/main/java/org/linphone/compatibility/Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Compatibility.kt index 846f0af4a..c1bd96a85 100644 --- a/app/src/main/java/org/linphone/compatibility/Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Compatibility.kt @@ -23,6 +23,7 @@ import android.app.Activity import android.app.Notification import android.app.PendingIntent import android.app.Service +import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.graphics.Bitmap @@ -290,7 +291,7 @@ class Compatibility { } } - fun startForegroundService(service: Service, notifId: Int, notif: Notification?) { + private fun startForegroundService(service: Service, notifId: Int, notif: Notification?) { if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) { Api31Compatibility.startForegroundService(service, notifId, notif) } else { @@ -298,6 +299,22 @@ class Compatibility { } } + fun startCallForegroundService(service: Service, notifId: Int, notif: Notification) { + if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) { + Api34Compatibility.startCallForegroundService(service, notifId, notif) + } else { + startForegroundService(service, notifId, notif) + } + } + + fun startDataSyncForegroundService(service: Service, notifId: Int, notif: Notification) { + if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) { + Api34Compatibility.startDataSyncForegroundService(service, notifId, notif) + } else { + startForegroundService(service, notifId, notif) + } + } + /* Call */ fun canDrawOverlay(context: Context): Boolean { @@ -455,5 +472,26 @@ class Compatibility { } return false } + + fun clearClipboard(clipboard: ClipboardManager) { + if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) { + Api28Compatibility.clearClipboard(clipboard) + } + } + + fun hasFullScreenIntentPermission(context: Context): Boolean { + if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) { + return Api34Compatibility.hasFullScreenIntentPermission(context) + } + return true + } + + fun requestFullScreenIntentPermission(context: Context): Boolean { + if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) { + Api34Compatibility.requestFullScreenIntentPermission(context) + return true + } + return false + } } } diff --git a/app/src/main/java/org/linphone/contact/ContactDataInterface.kt b/app/src/main/java/org/linphone/contact/ContactDataInterface.kt index 79ea8205a..2402d92bd 100644 --- a/app/src/main/java/org/linphone/contact/ContactDataInterface.kt +++ b/app/src/main/java/org/linphone/contact/ContactDataInterface.kt @@ -88,7 +88,7 @@ abstract class GenericContactViewModel(private val sipAddress: Address) : Messag contactLookup() } - protected fun contactLookup() { + private fun contactLookup() { displayName.value = LinphoneUtils.getDisplayName(sipAddress) val friend = coreContext.contactsManager.findContactByAddress(sipAddress) if (friend != null) { diff --git a/app/src/main/java/org/linphone/contact/ContactsManager.kt b/app/src/main/java/org/linphone/contact/ContactsManager.kt index 7892a3f8f..07aafe7e8 100644 --- a/app/src/main/java/org/linphone/contact/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contact/ContactsManager.kt @@ -35,7 +35,6 @@ import androidx.core.app.Person import androidx.core.graphics.drawable.IconCompat import androidx.lifecycle.MutableLiveData import java.io.IOException -import kotlinx.coroutines.* import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R @@ -388,7 +387,7 @@ fun Friend.getContactForPhoneNumberOrAddress(value: String): String? { return null } -fun Friend.hasPresence(): Boolean { +fun Friend.hasLongTermPresence(): Boolean { for (address in addresses) { val presenceModel = getPresenceModelForUriOrTel(address.asStringUriOnly()) if (presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open) return true @@ -421,10 +420,12 @@ fun Friend.getPictureUri(thumbnailPreferred: Boolean = false): Uri? { // Check that the URI points to a real file val contentResolver = coreContext.context.contentResolver try { - if (contentResolver.openAssetFileDescriptor(pictureUri, "r") != null) { + val fd = contentResolver.openAssetFileDescriptor(pictureUri, "r") + if (fd != null) { + fd.close() return pictureUri } - } catch (ioe: IOException) { } + } catch (_: IOException) { } } // Fallback to thumbnail if high res picture isn't available @@ -432,11 +433,11 @@ fun Friend.getPictureUri(thumbnailPreferred: Boolean = false): Uri? { lookupUri, ContactsContract.Contacts.Photo.CONTENT_DIRECTORY ) - } catch (e: Exception) { } + } catch (_: Exception) { } } else if (photo != null) { try { return Uri.parse(photo) - } catch (e: Exception) { } + } catch (_: Exception) { } } return null } diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt index c80b6d605..00d1fc259 100644 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ b/app/src/main/java/org/linphone/core/CoreContext.kt @@ -165,12 +165,6 @@ class CoreContext( } } - override fun onAuthenticationRequested(core: Core, authInfo: AuthInfo, method: AuthMethod) { - Log.w( - "[Context] Authentication requested for account [${authInfo.username}@${authInfo.domain}] with realm [${authInfo.realm}] using method [$method]" - ) - } - override fun onPushNotificationReceived(core: Core, payload: String?) { Log.i("[Context] Push notification received: $payload") } @@ -431,18 +425,22 @@ class CoreContext( } fun onForeground() { - // If presence publish is disabled and we call core.setConsolidatedPresence, it will enabled it! - if (core.defaultAccount?.params?.isPublishEnabled == true) { - Log.i("[Context] App is in foreground, setting consolidated presence to Online") + // We can't rely on defaultAccount?.params?.isPublishEnabled + // as it will be modified by the SDK when changing the presence status + if (corePreferences.publishPresence) { + Log.i("[Context] App is in foreground, PUBLISHING presence as Online") core.consolidatedPresence = ConsolidatedPresence.Online } } fun onBackground() { - // If presence publish is disabled and we call core.setConsolidatedPresence, it will enabled it! - if (core.defaultAccount?.params?.isPublishEnabled == true) { - Log.i("[Context] App is in background, setting consolidated presence to Busy") - core.consolidatedPresence = ConsolidatedPresence.Busy + // We can't rely on defaultAccount?.params?.isPublishEnabled + // as it will be modified by the SDK when changing the presence status + if (corePreferences.publishPresence) { + Log.i("[Context] App is in background, un-PUBLISHING presence info") + // We don't use ConsolidatedPresence.Busy but Offline to do an unsubscribe, + // Flexisip will handle the Busy status depending on other devices + core.consolidatedPresence = ConsolidatedPresence.Offline } } @@ -1039,8 +1037,8 @@ class CoreContext( val extension = FileUtils.getExtensionFromFileName(filePath) val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - when { - FileUtils.isMimeImage(mime) -> { + when (FileUtils.getMimeType(mime)) { + FileUtils.MimeType.Image -> { if (Compatibility.addImageToMediaStore(context, content)) { Log.i( "[Context] Successfully exported image [${content.name}] to Media Store" @@ -1051,7 +1049,7 @@ class CoreContext( ) } } - FileUtils.isMimeVideo(mime) -> { + FileUtils.MimeType.Video -> { if (Compatibility.addVideoToMediaStore(context, content)) { Log.i( "[Context] Successfully exported video [${content.name}] to Media Store" @@ -1062,7 +1060,7 @@ class CoreContext( ) } } - FileUtils.isMimeAudio(mime) -> { + FileUtils.MimeType.Audio -> { if (Compatibility.addAudioToMediaStore(context, content)) { Log.i( "[Context] Successfully exported audio [${content.name}] to Media Store" diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 9d88e0a32..9a19daf03 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -53,6 +53,7 @@ class CorePreferences constructor(private val context: Context) { context, MasterKey.DEFAULT_MASTER_KEY_ALIAS ).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build() + try { EncryptedSharedPreferences.create( context, @@ -280,6 +281,12 @@ class CorePreferences constructor(private val context: Context) { config.setBool("app", "contact_shortcuts", value) } + var publishPresence: Boolean + get() = config.getBool("app", "publish_presence", true) + set(value) { + config.setBool("app", "publish_presence", value) + } + /* Call */ var sendEarlyMedia: Boolean diff --git a/app/src/main/java/org/linphone/core/CoreService.kt b/app/src/main/java/org/linphone/core/CoreService.kt index a8bfead64..5cc609cfd 100644 --- a/app/src/main/java/org/linphone/core/CoreService.kt +++ b/app/src/main/java/org/linphone/core/CoreService.kt @@ -34,27 +34,33 @@ class CoreService : CoreService() { } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.i("[Service] Ensuring Core exists") + Log.i("[Service] Starting, ensuring Core exists") + if (corePreferences.keepServiceAlive) { Log.i("[Service] Starting as foreground to keep app alive in background") - if (!ensureCoreExists( - applicationContext, - pushReceived = false, - service = this, - useAutoStartDescription = false - ) - ) { + val contextCreated = ensureCoreExists( + applicationContext, + pushReceived = false, + service = this, + useAutoStartDescription = false + ) + if (!contextCreated) { + // Only start foreground notification if context already exists, otherwise context will do it itself coreContext.notificationsManager.startForeground(this, false) } } else if (intent?.extras?.get("StartForeground") == true) { Log.i("[Service] Starting as foreground due to device boot or app update") - if (!ensureCoreExists( - applicationContext, - pushReceived = false, - service = this, - useAutoStartDescription = true - ) - ) { + val contextCreated = ensureCoreExists( + applicationContext, + pushReceived = false, + service = this, + useAutoStartDescription = true, + skipCoreStart = true + ) + if (contextCreated) { + coreContext.start() + } else { + // Only start foreground notification if context already exists, otherwise context will do it itself coreContext.notificationsManager.startForeground(this, true) } coreContext.checkIfForegroundServiceNotificationCanBeRemovedAfterDelay(5000) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index c5d6f4594..bdd1b30e7 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -84,7 +84,7 @@ class NotificationsManager(private val context: Context) { const val INTENT_REMOTE_ADDRESS = "REMOTE_ADDRESS" private const val SERVICE_NOTIF_ID = 1 - private const val MISSED_CALLS_NOTIF_ID = 2 + private const val MISSED_CALLS_NOTIF_ID = 10 const val CHAT_TAG = "Chat" private const val MISSED_CALL_TAG = "Missed call" @@ -179,7 +179,23 @@ class NotificationsManager(private val context: Context) { } } - val notifiable = createChatNotifiable(room, messages) + var allOutgoing = true + for (message in messages) { + if (!message.isOutgoing) { + allOutgoing = false + break + } + } + + val notifiable = getNotifiableForRoom(room) + val updated = updateChatNotifiableWithMessages(notifiable, room, messages) + if (!updated) { + Log.w( + "[Notifications Manager] No changes made to notifiable, do not display it again" + ) + return + } + if (notifiable.messages.isNotEmpty()) { displayChatNotifiable(room, notifiable) } else { @@ -333,7 +349,7 @@ class NotificationsManager(private val context: Context) { Log.w( "[Notifications Manager] Found existing call? notification [${notification.id}], cancelling it" ) - manager.cancel(notification.tag, notification.id) + manager.cancel(notification.id) } else if (notification.tag == CHAT_TAG) { Log.i( "[Notifications Manager] Found existing chat notification [${notification.id}]" @@ -381,7 +397,18 @@ class NotificationsManager(private val context: Context) { } Log.i("[Notifications Manager] Notifying [$id] with tag [$tag]") - notificationManager.notify(tag, id, notification) + try { + notificationManager.notify(tag, id, notification) + } catch (iae: IllegalArgumentException) { + if (service == null && tag == null) { + // We can't notify using CallStyle if there isn't a foreground service running + Log.w( + "[Notifications Manager] Foreground service hasn't started yet, can't display a CallStyle notification until then: $iae" + ) + } else { + Log.e("[Notifications Manager] Exception occurred: $iae") + } + } } fun cancel(id: Int, tag: String? = null) { @@ -460,24 +487,23 @@ class NotificationsManager(private val context: Context) { fun startForeground(coreService: CoreService, useAutoStartDescription: Boolean = true) { service = coreService - if (serviceNotification == null) { - createServiceNotification(useAutoStartDescription) - if (serviceNotification == null) { - Log.e( - "[Notifications Manager] Failed to create service notification, aborting foreground service!" - ) - return - } + val notification = serviceNotification ?: createServiceNotification(useAutoStartDescription) + if (notification == null) { + Log.e( + "[Notifications Manager] Failed to create service notification, aborting foreground service!" + ) + return } currentForegroundServiceNotificationId = SERVICE_NOTIF_ID Log.i( "[Notifications Manager] Starting service as foreground [$currentForegroundServiceNotificationId]" ) - Compatibility.startForegroundService( + + Compatibility.startDataSyncForegroundService( coreService, currentForegroundServiceNotificationId, - serviceNotification + notification ) } @@ -491,7 +517,7 @@ class NotificationsManager(private val context: Context) { val coreService = service if (coreService != null) { - Compatibility.startForegroundService( + Compatibility.startCallForegroundService( coreService, currentForegroundServiceNotificationId, callNotification @@ -552,11 +578,11 @@ class NotificationsManager(private val context: Context) { service = null } - private fun createServiceNotification(useAutoStartDescription: Boolean = false) { + private fun createServiceNotification(useAutoStartDescription: Boolean = false): Notification? { val serviceChannel = context.getString(R.string.notification_channel_service_id) if (Compatibility.getChannelImportance(notificationManager, serviceChannel) == NotificationManagerCompat.IMPORTANCE_NONE) { Log.w("[Notifications Manager] Service channel is disabled!") - return + return null } val pendingIntent = NavDeepLinkBuilder(context) @@ -588,7 +614,9 @@ class NotificationsManager(private val context: Context) { builder.setContentIntent(pendingIntent) } - serviceNotification = builder.build() + val notif = builder.build() + serviceNotification = notif + return notif } /* Call related */ @@ -846,14 +874,18 @@ class NotificationsManager(private val context: Context) { notify(notifiable.notificationId, notification, CHAT_TAG) } - private fun createChatNotifiable(room: ChatRoom, messages: Array): Notifiable { - val notifiable = getNotifiableForRoom(room) - + private fun updateChatNotifiableWithMessages( + notifiable: Notifiable, + room: ChatRoom, + messages: Array + ): Boolean { + var updated = false for (message in messages) { if (message.isRead || message.isOutgoing) continue val friend = coreContext.contactsManager.findContactByAddress(message.fromAddress) val notifiableMessage = getNotifiableMessage(message, friend) notifiable.messages.add(notifiableMessage) + updated = true } if (room.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) { @@ -862,7 +894,7 @@ class NotificationsManager(private val context: Context) { notifiable.isGroup = true notifiable.groupTitle = room.subject } - return notifiable + return updated } private fun createChatReactionNotifiable( diff --git a/app/src/main/java/org/linphone/utils/AppUtils.kt b/app/src/main/java/org/linphone/utils/AppUtils.kt index aeb738b20..88d61b66e 100644 --- a/app/src/main/java/org/linphone/utils/AppUtils.kt +++ b/app/src/main/java/org/linphone/utils/AppUtils.kt @@ -44,6 +44,10 @@ import org.linphone.core.tools.Log * Various utility methods for application */ class AppUtils { + interface KeyboardVisibilityListener { + fun onKeyboardVisibilityChanged(visible: Boolean) + } + companion object { private val emojiCompat: EmojiCompat? get() = initEmojiCompat() diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index b4f72f1c2..75db99224 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -73,7 +73,7 @@ fun View.hideKeyboard() { } catch (_: Exception) {} } -fun View.addKeyboardInsetListener(lambda: (visible: Boolean) -> Unit) { +fun View.setKeyboardInsetListener(lambda: (visible: Boolean) -> Unit) { doOnLayout { var isKeyboardVisible = ViewCompat.getRootWindowInsets(this)?.isVisible( WindowInsetsCompat.Type.ime() @@ -81,8 +81,9 @@ fun View.addKeyboardInsetListener(lambda: (visible: Boolean) -> Unit) { lambda(isKeyboardVisible) + // See https://issuetracker.google.com/issues/281942480 ViewCompat.setOnApplyWindowInsetsListener( - this + rootView ) { view, insets -> val keyboardVisibilityChanged = ViewCompat.getRootWindowInsets(view) ?.isVisible(WindowInsetsCompat.Type.ime()) == true @@ -90,7 +91,7 @@ fun View.addKeyboardInsetListener(lambda: (visible: Boolean) -> Unit) { isKeyboardVisible = keyboardVisibilityChanged lambda(isKeyboardVisible) } - insets + ViewCompat.onApplyWindowInsets(view, insets) } } } @@ -343,7 +344,7 @@ fun setImageViewScaleType(imageView: ImageView, scaleType: ImageView.ScaleType) @BindingAdapter("coilRounded") fun loadRoundImageWithCoil(imageView: ImageView, path: String?) { - if (path != null && path.isNotEmpty() && FileUtils.isExtensionImage(path)) { + if (!path.isNullOrEmpty() && FileUtils.isExtensionImage(path)) { imageView.load(path) { transformations(CircleCropTransformation()) } @@ -354,7 +355,7 @@ fun loadRoundImageWithCoil(imageView: ImageView, path: String?) { @BindingAdapter("coil") fun loadImageWithCoil(imageView: ImageView, path: String?) { - if (path != null && path.isNotEmpty() && FileUtils.isExtensionImage(path)) { + if (!path.isNullOrEmpty() && FileUtils.isExtensionImage(path)) { if (corePreferences.vfsEnabled && path.startsWith(corePreferences.vfsCachePath)) { imageView.load(path) { diskCachePolicy(CachePolicy.DISABLED) @@ -551,9 +552,23 @@ fun loadAvatarWithCoil(imageView: ImageView, path: String?) { @BindingAdapter("coilVideoPreview") fun loadVideoPreview(imageView: ImageView, path: String?) { - if (path != null && path.isNotEmpty() && FileUtils.isExtensionVideo(path)) { + if (!path.isNullOrEmpty() && FileUtils.isExtensionVideo(path)) { imageView.load(path) { videoFrameMillis(0) + listener( + onError = { _, result -> + Log.e( + "[Data Binding] [Coil] Error getting preview picture from video? [$path]: ${result.throwable}" + ) + }, + onSuccess = { _, _ -> + // Display "play" button above video preview + LayoutInflater.from(imageView.context).inflate( + R.layout.video_play_button, + imageView.parent as ViewGroup + ) + } + ) } } } @@ -582,13 +597,23 @@ fun addPhoneNumberEditTextValidation(editText: EditText, enabled: Boolean) { fun addPrefixEditTextValidation(editText: EditText, enabled: Boolean) { if (!enabled) return editText.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable?) {} + override fun afterTextChanged(s: Editable?) { + val dialPlan = PhoneNumberUtils.getDialPlanFromCountryCallingPrefix( + s.toString().substring(1) + ) + if (dialPlan == null) { + editText.error = + editText.context.getString( + R.string.assistant_error_invalid_international_prefix + ) + } + } override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} @SuppressLint("SetTextI18n") override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - if (s == null || s.isEmpty() || !s.startsWith("+")) { + if (s.isNullOrEmpty() || !s.startsWith("+")) { editText.setText("+$s") } } diff --git a/app/src/main/java/org/linphone/utils/FileUtils.kt b/app/src/main/java/org/linphone/utils/FileUtils.kt index f15260a19..7300364c1 100644 --- a/app/src/main/java/org/linphone/utils/FileUtils.kt +++ b/app/src/main/java/org/linphone/utils/FileUtils.kt @@ -26,7 +26,10 @@ import android.content.Intent import android.database.CursorIndexOutOfBoundsException import android.net.Uri import android.os.Environment +import android.os.ParcelFileDescriptor +import android.os.Process.myUid import android.provider.OpenableColumns +import android.system.Os.fstat import android.webkit.MimeTypeMap import androidx.core.content.FileProvider import java.io.* @@ -42,6 +45,15 @@ import org.linphone.R import org.linphone.core.tools.Log class FileUtils { + enum class MimeType { + PlainText, + Pdf, + Image, + Video, + Audio, + Unknown + } + companion object { fun getNameFromFilePath(filePath: String): String { var name = filePath @@ -64,36 +76,28 @@ class FileUtils { return extension.lowercase(Locale.getDefault()) } - fun isMimePlainText(type: String?): Boolean { - return type?.startsWith("text/plain") ?: false - } - - fun isMimePdf(type: String?): Boolean { - return type?.startsWith("application/pdf") ?: false - } - - fun isMimeImage(type: String?): Boolean { - return type?.startsWith("image/") ?: false - } - - fun isMimeVideo(type: String?): Boolean { - return type?.startsWith("video/") ?: false - } - - fun isMimeAudio(type: String?): Boolean { - return type?.startsWith("audio/") ?: false + fun getMimeType(type: String?): MimeType { + if (type.isNullOrEmpty()) return MimeType.Unknown + return when { + type.startsWith("image/") -> MimeType.Image + type.startsWith("text/plain") -> MimeType.PlainText + type.startsWith("video/") -> MimeType.Video + type.startsWith("audio/") -> MimeType.Audio + type.startsWith("application/pdf") -> MimeType.Pdf + else -> MimeType.Unknown + } } fun isExtensionImage(path: String): Boolean { val extension = getExtensionFromFileName(path) val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - return isMimeImage(type) + return getMimeType(type) == MimeType.Image } fun isExtensionVideo(path: String): Boolean { val extension = getExtensionFromFileName(path) val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - return isMimeVideo(type) + return getMimeType(type) == MimeType.Video } fun clearExistingPlainFiles() { @@ -269,6 +273,21 @@ class FileUtils { var result: String? = null val name: String = getNameFromUri(uri, context) + try { + if (fstat( + ParcelFileDescriptor.open( + File(uri.path), + ParcelFileDescriptor.MODE_READ_ONLY + ).fileDescriptor + ).st_uid != myUid() + ) { + Log.e("[File Utils] File descriptor UID different from our, denying copy!") + return result + } + } catch (e: Exception) { + Log.e("[File Utils] Can't check file ownership: ", e) + } + try { val localFile: File = createFile(name) val remoteFile = diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 8258581fb..fbcea8513 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -51,7 +51,7 @@ class LinphoneUtils { } val localDisplayName = account?.params?.identityAddress?.displayName // Do not return an empty local display name - if (localDisplayName != null && localDisplayName.isNotEmpty()) { + if (!localDisplayName.isNullOrEmpty()) { return localDisplayName } } @@ -179,7 +179,7 @@ class LinphoneUtils { fun deleteFilesAttachedToChatMessage(chatMessage: ChatMessage) { for (content in chatMessage.contents) { val filePath = content.filePath - if (filePath != null && filePath.isNotEmpty()) { + if (!filePath.isNullOrEmpty()) { Log.i("[Linphone Utils] Deleting file $filePath") FileUtils.deleteFile(filePath) } @@ -288,6 +288,11 @@ class LinphoneUtils { return true } + fun isFileTransferAvailable(): Boolean { + val core = coreContext.core + return core.fileTransferServer.orEmpty().isNotEmpty() + } + fun hashPassword( userId: String, password: String, diff --git a/app/src/main/res/layout-land/voip_conference_active_speaker.xml b/app/src/main/res/layout-land/voip_conference_active_speaker.xml index d3cdea714..8106f0a2b 100644 --- a/app/src/main/res/layout-land/voip_conference_active_speaker.xml +++ b/app/src/main/res/layout-land/voip_conference_active_speaker.xml @@ -181,13 +181,13 @@ android:contentDescription="@null" coilVoipContact="@{conferenceViewModel.speakingParticipant}" android:background="@drawable/generated_avatar_bg" - app:layout_constraintDimensionRatio="1:1" app:layout_constraintBottom_toBottomOf="@id/active_speaker_background" app:layout_constraintEnd_toEndOf="@id/active_speaker_background" app:layout_constraintHeight_max="@dimen/voip_contact_avatar_max_size" app:layout_constraintStart_toStartOf="@id/active_speaker_background" app:layout_constraintTop_toTopOf="@id/active_speaker_background" - app:layout_constraintWidth_max="@dimen/voip_contact_avatar_max_size" /> + app:layout_constraintWidth_max="@dimen/voip_contact_avatar_max_size" + app:layout_constraintDimensionRatio="1:1" /> + android:padding="5dp"> - + @@ -14,8 +16,15 @@ android:layout_margin="5dp" android:orientation="vertical"> + + + + - - - - - - - + android:textAlignment="center" + android:drawablePadding="5dp" + android:visibility="@{data.image || data.video ? View.GONE : View.VISIBLE, default=gone}" + android:text="@{data.fileName, default=`test.txt`}" + android:drawableTop="@{data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : @drawable/file), default=@drawable/file}" + tools:ignore="UseCompatTextViewDrawableXml" /> - + @@ -29,7 +31,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="5dp" - android:drawableTop="@{data.video ? @drawable/file_video : (data.image ? @drawable/file_picture : (data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : (data.voiceRecording ? @drawable/audio_recording_reply_preview_default : @drawable/file)))), default=@drawable/file}" android:drawablePadding="5dp" android:ellipsize="middle" android:gravity="center" @@ -37,7 +38,9 @@ android:onLongClick="@{longClickListener}" android:singleLine="true" android:text="@{data.fileName, default=`test.pdf`}" - android:textAlignment="center" /> + android:textAlignment="center" + android:drawableTop="@{data.video ? @drawable/file_video : (data.image ? @drawable/file_picture : (data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : (data.voiceRecording ? @drawable/audio_recording_reply_preview_default : @drawable/file)))), default=@drawable/file}" + tools:ignore="UseCompatTextViewDrawableXml" /> - + @@ -15,6 +17,8 @@ + android:textAlignment="center" + android:drawableTop="@{data.video ? @drawable/file_video : (data.image ? @drawable/file_picture : (data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : (data.voiceRecording ? @drawable/audio_recording_reply_preview_default : @drawable/file)))), default=@drawable/file}" + tools:ignore="UseCompatTextViewDrawableXml" /> diff --git a/app/src/main/res/layout/chat_message_image_content_cell.xml b/app/src/main/res/layout/chat_message_image_content_cell.xml index 631381f5f..aa47d9402 100644 --- a/app/src/main/res/layout/chat_message_image_content_cell.xml +++ b/app/src/main/res/layout/chat_message_image_content_cell.xml @@ -18,14 +18,14 @@ + android:visibility="@{data.chatMessage.outgoing || selectionListViewModel.isEditionEnabled || data.hideAvatar || data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" + tools:ignore="ContentDescription" /> + + - - + + - - - + @@ -16,14 +17,29 @@ + inflatedLifecycleOwner="@{true}" + android:background="?attr/backgroundColor"> + + - - \ No newline at end of file diff --git a/app/src/main/res/layout/chat_room_creation_fragment.xml b/app/src/main/res/layout/chat_room_creation_fragment.xml index 7f43d485b..9eb6ac052 100644 --- a/app/src/main/res/layout/chat_room_creation_fragment.xml +++ b/app/src/main/res/layout/chat_room_creation_fragment.xml @@ -163,6 +163,7 @@ android:layout_alignRight="@id/searchBar" android:layout_alignBottom="@id/searchBar" android:onClick="@{() -> viewModel.clearFilter()}" + android:contentDescription="@string/content_description_clear_field" android:src="@drawable/field_clean" /> + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> @@ -49,7 +50,8 @@ android:background="@drawable/led_background" android:padding="@dimen/contact_presence_badge_padding" app:presenceIcon="@{data.presenceStatus}" - android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" /> + android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" + tools:ignore="ContentDescription" /> + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> @@ -50,7 +51,8 @@ android:background="@drawable/led_background" android:padding="@dimen/contact_presence_badge_padding" android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" - app:presenceIcon="@{data.presenceStatus}" /> + app:presenceIcon="@{data.presenceStatus}" + tools:ignore="ContentDescription" /> + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> @@ -37,7 +38,8 @@ android:background="@drawable/led_background" android:padding="@dimen/contact_presence_badge_padding" app:presenceIcon="@{data.presenceStatus}" - android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" /> + android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" + tools:ignore="ContentDescription" /> + app:layout_constraintBottom_toBottomOf="@id/avatar" + tools:ignore="ContentDescription"/> + android:layout_gravity="start" + android:background="?attr/backgroundColor"> - - - - diff --git a/app/src/main/res/layout/conference_scheduling_summary_fragment.xml b/app/src/main/res/layout/conference_scheduling_summary_fragment.xml index 3a62d3abf..2e6fc3da4 100644 --- a/app/src/main/res/layout/conference_scheduling_summary_fragment.xml +++ b/app/src/main/res/layout/conference_scheduling_summary_fragment.xml @@ -102,6 +102,7 @@ android:layout_marginTop="5dp" android:layout_marginStart="5dp" android:layout_marginEnd="5dp" + android:inputType="textEmailSubject" style="@style/conference_scheduling_font" android:background="?attr/voipFormDisabledFieldBackgroundColor" android:text="@{viewModel.subject}" @@ -252,6 +253,7 @@ android:gravity="top" android:minLines="3" android:maxLines="5" + android:inputType="textMultiLine" style="@style/conference_scheduling_font" android:background="?attr/voipFormDisabledFieldBackgroundColor" android:hint="@string/conference_schedule_description_hint" diff --git a/app/src/main/res/layout/contact_detail_fragment.xml b/app/src/main/res/layout/contact_detail_fragment.xml index 002a24103..fe3f75ee4 100644 --- a/app/src/main/res/layout/contact_detail_fragment.xml +++ b/app/src/main/res/layout/contact_detail_fragment.xml @@ -105,7 +105,8 @@ android:background="@drawable/led_background" android:padding="@dimen/contact_presence_big_badge_padding" app:presenceIcon="@{viewModel.presenceStatus}" - android:visibility="@{viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" /> + android:visibility="@{viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" + tools:ignore="ContentDescription" /> diff --git a/app/src/main/res/layout/contact_list_cell.xml b/app/src/main/res/layout/contact_list_cell.xml index 302dc6586..33cb88757 100644 --- a/app/src/main/res/layout/contact_list_cell.xml +++ b/app/src/main/res/layout/contact_list_cell.xml @@ -1,6 +1,7 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> @@ -54,7 +55,8 @@ android:background="@drawable/led_background" android:padding="@dimen/contact_presence_badge_padding" app:presenceIcon="@{viewModel.presenceStatus}" - android:visibility="@{viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" /> + android:visibility="@{viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" + tools:ignore="ContentDescription" /> + android:visibility="@{viewModel.hasLongTermPresence ? View.VISIBLE : View.GONE, default=gone}" /> + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> @@ -52,7 +53,8 @@ android:background="@drawable/led_background" android:padding="@dimen/contact_presence_badge_padding" app:presenceIcon="@{data.presenceStatus}" - android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" /> + android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" + tools:ignore="ContentDescription" /> + android:layout_height="match_parent"> \ No newline at end of file diff --git a/app/src/main/res/layout/history_detail_fragment.xml b/app/src/main/res/layout/history_detail_fragment.xml index 280854a97..ff74d4bcb 100644 --- a/app/src/main/res/layout/history_detail_fragment.xml +++ b/app/src/main/res/layout/history_detail_fragment.xml @@ -107,7 +107,8 @@ android:background="@drawable/led_background" android:padding="@dimen/contact_presence_big_badge_padding" app:presenceIcon="@{viewModel.presenceStatus}" - android:visibility="@{viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" /> + android:visibility="@{viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" + bind:ignore="ContentDescription" /> diff --git a/app/src/main/res/layout/history_list_cell.xml b/app/src/main/res/layout/history_list_cell.xml index d25b0318b..fef2934ac 100644 --- a/app/src/main/res/layout/history_list_cell.xml +++ b/app/src/main/res/layout/history_list_cell.xml @@ -1,6 +1,7 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> @@ -103,7 +104,8 @@ android:background="@drawable/led_background" android:padding="@dimen/contact_presence_badge_padding" app:presenceIcon="@{viewModel.presenceStatus}" - android:visibility="@{viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" /> + android:visibility="@{viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" + tools:ignore="ContentDescription" /> diff --git a/app/src/main/res/layout/settings_account_fragment.xml b/app/src/main/res/layout/settings_account_fragment.xml index af9d48967..7ede66f9d 100644 --- a/app/src/main/res/layout/settings_account_fragment.xml +++ b/app/src/main/res/layout/settings_account_fragment.xml @@ -127,12 +127,6 @@ linphone:checked="@={viewModel.isDefault}" linphone:enabled="@{!viewModel.isDefault}"/> - - diff --git a/app/src/main/res/layout/settings_contacts_fragment.xml b/app/src/main/res/layout/settings_contacts_fragment.xml index 8e2287491..957fff8d6 100644 --- a/app/src/main/res/layout/settings_contacts_fragment.xml +++ b/app/src/main/res/layout/settings_contacts_fragment.xml @@ -71,6 +71,12 @@ linphone:checked="@={viewModel.friendListSubscribe}" linphone:enabled="@{viewModel.readContactsPermissionGranted && viewModel.rlsAddressAvailable}"/> + + + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> @@ -81,7 +82,8 @@ android:background="@drawable/led_background" android:padding="@dimen/contact_presence_badge_padding" app:presenceIcon="@{viewModel.presenceStatus}" - android:visibility="@{!viewModel.defaultAccountFound || viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" /> + android:visibility="@{!viewModel.defaultAccountFound || viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" + tools:ignore="ContentDescription" /> + \ No newline at end of file diff --git a/app/src/main/res/layout/voip_buttons_extra.xml b/app/src/main/res/layout/voip_buttons_extra.xml index 4e22786c2..36a7cf554 100644 --- a/app/src/main/res/layout/voip_buttons_extra.xml +++ b/app/src/main/res/layout/voip_buttons_extra.xml @@ -68,6 +68,7 @@ android:layout_centerInParent="true" android:gravity="center" android:onClick="@{() -> controlsViewModel.goToChat()}" + android:visibility="@{controlsViewModel.chatDisabled ? View.INVISIBLE : View.VISIBLE}" android:text="@string/call_action_chat" app:drawableTopCompat="@drawable/icon_call_chat" app:layout_constraintBottom_toTopOf="@id/calls" @@ -86,7 +87,7 @@ android:gravity="center" android:text="@{String.valueOf(callsViewModel.currentCallUnreadChatMessageCount), default=1}" android:translationY="@{controlsViewModel.bouncyCounterTranslateY}" - android:visibility="@{callsViewModel.currentCallUnreadChatMessageCount == 0 ? View.GONE : View.VISIBLE}" + android:visibility="@{callsViewModel.currentCallUnreadChatMessageCount == 0 || controlsViewModel.chatDisabled ? View.GONE : View.VISIBLE}" app:layout_constraintEnd_toEndOf="@id/chat" app:layout_constraintStart_toStartOf="@id/chat" app:layout_constraintTop_toTopOf="@id/chat" /> diff --git a/app/src/main/res/layout/voip_call_incoming_fragment.xml b/app/src/main/res/layout/voip_call_incoming_fragment.xml index 1663180ac..a9ae0f199 100644 --- a/app/src/main/res/layout/voip_call_incoming_fragment.xml +++ b/app/src/main/res/layout/voip_call_incoming_fragment.xml @@ -83,7 +83,8 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/incoming_call_timer" app:layout_constraintVertical_chainStyle="packed" - app:layout_constraintWidth_max="@dimen/voip_contact_avatar_max_size" /> + app:layout_constraintWidth_max="@dimen/voip_contact_avatar_max_size" + app:layout_constraintDimensionRatio="1:1" /> + app:layout_constraintWidth_max="@dimen/voip_contact_avatar_max_size" + app:layout_constraintDimensionRatio="1:1" /> + app:layout_constraintWidth_max="@dimen/voip_contact_avatar_max_size" + app:layout_constraintDimensionRatio="1:1" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintDimensionRatio="1:1" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintDimensionRatio="1:1" /> + app:layout_constraintWidth_max="@dimen/voip_contact_avatar_max_size" + app:layout_constraintDimensionRatio="1:1" /> + app:layout_constraintWidth_max="@dimen/voip_contact_avatar_max_size" + app:layout_constraintDimensionRatio="1:1"/> - - diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml new file mode 100644 index 000000000..40a117f58 --- /dev/null +++ b/app/src/main/res/values-bg/strings.xml @@ -0,0 +1,814 @@ + + +]> + + Премахнете контакт от разговора + Съобщенията в този разговор са краткотрайни + Към шифрован разговор + Промяна на снимката на контакта + Излезте от режима на редактиране + Гласова поща + Разговори + Към данните за контакта + Пропуснато обаждане + Изходящо обаждане + Сигурно + Ниско качество на обаждането + Обаждането не е сигурно + Приложение към среща + Отваряне на разговора в приложението + Записване на гласово съобщение + Спиране на записа на гласово съобщения + Поставяне на записа в пауза + Превъртете до долу или до първото непрочетено съобщение + Отворете контекстно меню за обаждането + Създайте нова среща + Копиране на адреса на срещата + Скриване на статистиката за обаждането + Участника си е изключил микрофона + Превключване на подробностите за срещата + Потребителя не желае да го безпокоят + Неуспешна връзка поради липсващи или неверни данни за автентикация +\n%s. +\n +\nМоля опитайте да въведете паролата отново или проверете настройките на акаута. + Изисква се за получаване на обаждания + &appName; контакти + Вижте нашата политика за поверителност + Логовете бяха изчистени + Телефонни номера + Име на държава или префикс + Потребителско име + Имейл + Домейн + SIP адреси + Отказ + Споделете връзка до логовете чрез… + Днес + Вчера + UDP + TCP + TLS + Свободния SIP клиент + Свързване в прогрес + Неуспешно свързване + За нас + Записи + Настройки + Обаждания + Няма последни обаждания + Няма групови обаждания + Желаете ли да изтриете този запис\? + Покана + Нямате контакти в адресната книга. + Нямате SIP контакти в адресната книга. + Търсене на контакти + Фамилия + Първо име + Организация + Изберете съществуващ контакт или добавете нов + Изберете къде да се съхрани този контакт + Локално съхранение + Този контакт не може да бъде изтрит + Налични са още резултати, моля редактирайте търсеното + Въведете номер или адрес + Включете логовете + Изключете логовете + Обаждането се трансферира + Не може да се трансферира обаждането + Няма разговори + Присъединихте се към група + Напуснахте група + %s се присъедини + %s напусна + ново устройство за %s + %s: + %s е администратор + %s вече не е администратор + Забелязана е MITM хакерска атака за %s + Нивото на сигурност намаля заради %s + Изключили сте краткотрайните съобщения + Включили сте краткотрайните съобщения: %s + Срок за изтичане на краткотрайните съобщения: %s + Изтегляне + Изберете източник + Информация + Участващи + Име на група + Администратор + Напускане + Вие сте администратор + Вече не сте администратор + Етап на доставка + Прочетено + Доставено + Недоставено + Изпратено + Отговорено + Препращане + Отговор + Етап на доставка + Изтриване + Добавяне към контакти + Файлът не съществува + Информация за групата + Устройства за чат + Към контакт + Краткотрайни съобщения + Изтриване на съобщения + Изключване на известия + Включване на известия + Изключено + 1 минута + 1 час + 1 ден + 3 дни + 1 седмица + + %s пише… + %s пишат… + + Неуспешно създаване на стая за чат + Неуспешно премахване на стая за чат + Сигурни ли сте, че искате да изтриете този разговор\? + <Редакция> + Няма подходящо приложение за такъв тип файл + Не може да се отвори шифрован файл в чата + Не може да покажем файла. + Експортиране + Отваряне като файл + Отговор + Съобщение + Натиснете бутона за да запишете гласово съобщение + Съобщението ще бъде изтрито + Прекъсване + Моля изчакайте да завърши първото изтегляне преди да започнете ново + Гласово съобщение + Онлайн днес в + Онлайн вчера в + Онлайн в + В отсъствие + Не безпокойте + Експортиране на запис чрез… + Среща + Насрочване на среща + Стартирайте групово обаждане + Задължително + Тема + Тема на групово обаждане + Адрес на среща + Добавете описание + Описание + Дата + Час + Часова зона + Изпратете покана чрез &appName; + Изпратете покана чрез имейл + Изпратете актуализация чрез имейл + Желаете ли да зашифровате срещата\? + Списък с участници + Организатор + Информация за срещата + Информация за груповото обаждане + Насрочване на среща + Стартирайте групово обаждане + В момента не участвате в срещата. + Натиснете бутона за да се присъедините отново. + Далечно групово обаждане + Локално групово обаждане + Покана за среща: + Срещата бе отменена: + Описание + Присъединяване + %d участника + Режим мозайка + Видеото е изключено + Няма прекратени срещи. + Организатор: + Стая за срещата + %s е администратор + Бяхте поканени на среща + покана за среща + Първи се присъединихте към груповото обаждане + Всички други участници напуснаха груповото обаждане + Входящо групово обаждане + Участници (%d) + Стартиране + Прекратяване + Насрочване + Желаете ли да изтриете тази среща\? + Желаете ли да изтриете тези срещи\? + Конференцията бе прекратена от организатора + Вие прекратихте конференцията + Входящо обаждане + Обаждането e в пауза + Изходящо обаждане + Активно обаждане + Абоната е зает + Не е открит такъв абонат + Няма достъп до мрежа + Услугата не е достъпна или грешка с мрежата + Отказ поради прекомерни заявки + Временно недостъпно + Грешка: %s + Списък с участници + Чат + Списък с обаждания + Циферблат + Промяна на оформление + Статистика за обаждания + Възобновяване на обаждането + Трансфериране на обаждането + Това обаждане се записва. + Обаждането е в пауза от ответната страна. + Обаждането е в пауза. + Записана снимка: %s + Аудио + Видео + Кодек: + IP семейство: + Ширина на качване: + Приблизителна ширина на теглене: + ICE свързаност: + Резолюция на изходящо видео: + Честота на кадрите на изходящо видео: + Загуба при изпращане: + Загуба при получаване: + Енкодер: + Декодер: + Филтър за възпроизвеждане: + Филтър за записване: + Шифроващ алгоритъм: + Алгоритъм за споразумение: + Алгоритъм за хаширане: + SAS алгоритъм: + Добре дошли + Създаване на акаунт + Използване на &appName; акаунт + Използване на SIP акаунт + Изтегляне на настройки от мрежата + Калибрира се настройката за заглушаване на ехо + общите условия + политиката за поверителност + Приемам %1$ и %2$ на Belledonne Communications + Разбрах + Откриване на нов акаунт чрез вашия имейл адрес: + Моля въведете само числа тук + Потребителското име е заето + Вече съществува акаунт със същия номер + Непозволени символи + Паролите не са еднакви + Имейл адреса не е валиден + Потребителското име е прекалено дълго + Акаунта не съществува или паролата е различна + Използвайте вашето потребителско име и парола вместо телефонен номер + Забравена парола\? + Код за потвърждение + Продължаване + Екранно име (незадължително) + Потвърдете паролата + След като сте готови, моля върнете се тази страница и натиснете бутона. + За да потвърдите вашия телефонен номер, моля въведете четири цифрения код: +\n + Пропускане + Свържете акаунт + Дистанционно изтегляне на настройки + Моля въведете URL адрес за настройки + URL + Неизвестен URL формат, неуспешно изтегляне на профил… + Неуспешно изтегляне или прилагане на профил… + Изтегляне и прилагане + QR код + SIP акаунти + Настройки + Тунел + Аудио + Видео + Обаждане + Чат + Мрежа + Контакти + Разширени + Срещи + Екранно име + Потребителско име + Парола + Валидиране + Невалидна парола! + Софтуерно анулиране на ехо + Премахва ехото което се чува от ответната страна + Калибриране за анулиране на ехо + Започва калибриране за анулиране на ехо + Калибрирано за %s милисекунди + Неуспешно калибриране + Тест за ехо + Натиснете за да започне тест за ехо + Теста за ехо е спрян + Теста за ехо се извършва + Звуково устройство по подразбиране + Изходно звуково устройство по подразбиране + Ограничение на битрейт за кодека + (в децибели) + Усилване на звука + Кодекси + Включване на видео + Показване на камерата върху циферблата + Винаги изпращайте заявки за видео + Приемане на входящи заявки за видео + Винаги приемайте заявки за видео + Камера + Предпочитана честота на кадрите + Кодекси + Мелодия + Вибрация при входящи обаждания + Шифроване на медията + Няма + Подобряване на връзката с bluetooth устройства + Наслагване на известието за обаждане + Показване на известието извън приложението + SRTP + ZRTP + Квантово устойчив ZRTP + DTLS-SRTP + Незабавно стартиране на обаждането + Автоматично отговаряне на входящи обаждания + Времетраене за автоматичен отговор + в милисекунди + Времетраене за отказ на прекомерни входящи обаждания + в секунди + URI за гласова поща + Приемане на ранна медия + Звънене по време на входяща ранна медия + Настройки на известия за Андроид + Автоматично записване на обажданията + Известие когато обаждането се записва от кореспондент + Определя дали да се изтеглят файлове прикачени към получени съобщения + Винаги + Никога + Максимален размер + в байтове + Изтеглените файлове да са публични + Скриване на съдържанието на съобщения в известията + Показва се само името на автора + Създаване на преки пътища до стаи при стартиране + Скриване на празни стаи + Скриване на стаите от премахнат акаунт + Ако ви липсват стаи, опитайте да изключите тази настройка + Винаги отваряйте файлове в това приложение + Пак ще можете да ги експортирате към други приложения + Използване само през WiFi + Използване на случайни портове + SIP порт + Информация за присъствие на контакти + Показване на данни за организация + Създаване на преки пътища до контакти при стартиране + Заменя преките пътища до стаи, ако съществуват такива + Ако се изключи, контактите ще се съхраняват локално + Нова конфигурация LDAP + Включване + Изтриване + Връзка + URL адрес на сървър + Парола + Начин за автентикация + Прост + Чрез TLS + Проверка на сертификати + Автоматично + Изключено + Включено + Търсене + Не може да е празно! + Филтър + Максимален брой резултати + Време за изчакване + Време между две заявки + В милисекунди + Минимален брой символи за търсене + Разбор + Атрибути на SIP + Домейн + LDAP + Обвързване на DN + Отстраняване на грешки + Фонов режим + Показване на известие за да не се изключи приложението + Тъмна тема + Автоматично + Не + Да + Анимации + Име на устройството + URL адрес за доставка + URL за качване на логовете + Моля не редактирате ако не сте сигурни! + Андроид + Настройки на диспечер за захранването + Настройки за Андроид приложение + Зашифроване на всичко + Изпращане на логовете + Анулиране на логовете + Настройка за отстраняване на грешки + Други настройки + Позволяване на записване на екрана когато съдържа поверителна информация + Име на хост + Порт + Двоен режим включен + Име на хост на втори сървър + Изисква се в двоен режим + Втори порт на сървър + Изисква се в двоен режим + Режим + Изключен + Автоматично + Потребителско име + Име за автентикация + Въведете име за автентикация ако се различава от потребителското име (незадължително) + Домейн + Пример: sip.example.org ако акаунта ви е john@sip.example.org + Екранно име + Управление + Деактивиране + Използване по подразбиране + Изтриване + Разширени настройки + Разрешете известия + Транспортен протокол + SIP прокси + Изходящо прокси + Насочете всички обаждания през посоченото SIP прокси + STUN/TURN сървър + Включете ICE + в секунди (между 1 и 5) + Време за изтичане + Префикс за вашата държава + без + + Добавяне на префикс при изходящи обаждания и чат + Замяна на + с 00 + Конферентен фактори URI + Адрес на сървър за E2E шифроване + Адрес на сървър за E2E шифроване + Желаете ли да изтриете вашия акаунт\? + UDP + TCP + TLS + DTLS + AVPF + Деактивирайте пакетния режим + &appName; известия за входящи обаждания + Входящо обаждане + Входящо групово обаждане + Прекратяване + Отговор + Отговор + Маркиране като прочетено + Пропуснато обаждане + %d пропуснати обаждания + Сигурност на комуникацията + По-късно + Правилно + Кажете: + Вашия кореспондент трябва да каже: + Не показвай отново + Отказ + Изтриване + Добре + Приемане + Отхвърляне + Обаждане + Операцията се извършва, моля изчакайте + Сигурни ли сте, че искате да изтриете този предмет\? + Сигурни ли сте, че искате да изтриете избраните предмети\? + Съществува актуализация + Желаете ли да го отворите като текстов файл\? + Потвърждение + Добавяне на поле за SIP адрес + Добавяне на поле за телефонен номер + Назад + Изтрийте полето до него + Сменете между предна или задна камера + Пауза на обаждането + Прекратяване на обаждането + Отговорете на обаждането + Изключване на микрофона + Включване на микрофона + Пренасочване на звука през слушалки + Назад към обаждането + Включване на видео + Изключване на видео + Използване на bluetooth слушалки за звук + Използване на слушалки за звук + Стартиране на ново обаждане + Трансфериране на обаждането до друг + Файлов трансфер в очакване + Прикачен файл + Етап на доставка + Препращане на съобщението в този разговор + Съобщението е краткотрайно + Контакта е маркиран + Шифроване от \"край до край\" е изключено + Показване на списък с всички контакти + Показване на списък само със SIP контакти + Размаркиране на контакт + Стартиране на обаждане + Показване на менюто за стая + Влезте в режим на редактиране + Прикачване на файл към съобщението + Изисква се разрешение за да се показват входящи обаждания + Покавзане + Избрано времетраене за краткотрайни съобщения + Променете времетраенето за краткотрайни съобщения + Създайте стая за чат + Добавете участници + Контакта е администратор в този разговор + Контакта не е администратор в този разговор + Известията за този разговор са изключени + Контакта може да бъде поканен в шифровани разговори + Към разговор + Редактиране на контакт + Изтриване на контакт + Отхвърлете промените + Запишете промените + Промяна на собствената снимка + Към подробности за обаждането + Маркирайте всички в листа + Размаркирайте всички + Изтрийте всички маркирани + Стартиране или прекъсване на записа + Показване или скриване на страничното меню + Последни обаждания + Контакти + Циферблат + Премахване на последния символ + Създаване на контакт + Показване на всички обаждания + Показване само на пропуснатите обаждания + Входящо обаждане + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 0 + * + # + Създаване на нов разговор + Създаване на нов групов разговор + Несигурно + Шифровано + Много ниско качество на обаждането + Средно качество на обаждането + Добро качество на обаждането + Максимално качество на обаждането + Обаждането е сигурно + Сигурността на обаждането се определя + Приложение към видео + Записване на снимка от получен видео клип + Затваряне на известието + Отваряне на файла с друго приложение + Прекратяване на препращането + Прекратяване на споделянето + Прекратяване на отговора + Прекратяване на записа + Записване + Редактиране на среща + Изтриване на среща + Срещата ще бъде шифрована + Експортиране на събитието до календара + Показване на циферблата + Скриване на циферблата + Към списъка с участници + Показване или скриване на груповото офомление + Участника временно напусна груповото обаждане + Участници в груповото обаждане + Експортиране на запис + Потребителя е на линия + Потребителя не е на линия + Превключване на менюто с emoji + &appName; услуга + &appName; бе стартирано автоматично + Помогнете да преведем &appName; на различни езици + Неуспешно качване на логове! + Адреса на логовете бе копиран в клипборда + Телефонен номер + Изберете вашата държава + Префикс + Изберете държава + Парола + SIP адрес + + %d ден + %d дни + + Неочаквана грешка… + Транспортен протокол + Свързано + Не е свързано + Не е настроен акаунт + Асистент + Напускане + Няма изпуснати обаждания + Желаете ли да изтриете тези записи\? + Здравейте, свържете се с мен чрез &appName;! Може да изтеглите безплатното приложение от %s + Няма разрешение да се добавят или редактират контакти + Желаете ли да изтриете този контакт\? +\nСъщия ще бъде премахнат от адресната книга на вашето устройство + Желаете ли да изтриете тези контакти\? +\nСъщите ще бъде премахнати от адресната книга на вашето устройство + Отстраняване на грешки + Изпратете логовете + Вижте конфигурационния файл + Ответната страна пише… + устройството за %s беше премахнато + нова тема: %s + Смени се ключ за LIME идентичност за %s + Максималния брой участници е надвишен с %s + Устройства за чат + Напускане на група + Желаете ли да напуснете разговора\? + Пренасочено + Повторно изпращане + Копиране на текста + Желаете ли па препратите съобщението до тази стая\? + Изберете или създайте нов разговор за да препратите съобщението + Изберете или създайте нов разговор за да споделите файла/файловете + Изберете или създайте нов разговор за да споделите текста + Краткотрайни съобщения + Това съобщение ще бъде заличено от двете страни след като бъде прочетено и след като измине посочения срок. + Сигурни ли сте, че искате да изтриете тези разговори\? + Желаете ли да отворите файла като текст или да го експортирате (разшифрован) към друго приложение\? + Нивото на звук ви е ниско, моля опитайте да го увеличите + + %1$d непрочетено съобщение + %1$d непрочетени съобщения + + Вие получихте гласово съобщение + Онлайн + Няма записи + Желаете ли да насрочите среща за по-късно\? + Тема на срещата + Времетраене + Изпратете актуализация чрез &appName; + Поканата ще бъде изпратена чрез моят &appName; акаунт + Редактиране на среща + Адреса на срещата бе копиран в клипборда + Неуспешно изпращане на информация за срещата до участник + Срещата бе обновена: + Режим активен говорител + Режим само звук + Стартиране + Прекратяване + Срещи + Не може да промените оформлението на груповото обаждане защото има прекалено много участници + Има прекалено много участници за оформление в мозайка, преминаване към режим на активен говорител + (в пауза) + Няма насрочени срещи. + Неуспешно създаване на среща + %s вече не е администратор + Срещата е насрочена + Информацията за срещата е заличена + В момента сте сами в това групово обаждане + Видеото бе изключено поради ниска скорост на връзката + Групово обаждане + Желаете ли да започнете групово обаждане\? +\nВсички в тази група ще получат обаждане за да се присъединят към срещата. + Изходящо обаждане + Обаждането бе отказано + Несъвместими настройки на медия + Кореспондента желае да включи видео + Ново обаждане + Трансфериране на обаждане + Трансфериране с присъствие + Пауза на обаждането + Отговаряне на обаждането + Прекратяване на обаждането + Натиснете на бутона за да възобновите обаждането. + Ширина на теглене: + Резолюция на входящо видео: + Честота на кадрите на входящо видео: + Джитър буфер: + Шифроване на медия: + Алгоритъм за автентикация: + Този асистент ще ви помогне да настроите вашия SIP акаунт. + За какво се използва моят телефонен номер\? + " +\nВашите приятели ще ви открият по-лесно ако свържете телефонен номер с вашия акаунт +\n +\nЩе може да видите в вашата адресна книга кой използва &appName; и вашите приятели ще узнаят, че мога да ви се обадят чрез &appName; +\n" + Вашите приятели ще ви открият по-лесно ако свържете телефонен номер с вашия акаунт +\n +\nЩе може да видите в вашата адресна книга кой използва &appName; и вашите приятели ще узнаят, че мога да ви се обадят чрез &appName;. + Някои от опциите като групов чат и краткотрайни съобщения изискват &appName; акаунт. +\n +\nТези опции са скрити ако се регистрирате със SIP акаунт от друг доставчик. +\n +\nМоля свържете се с нас ако смятате да използвате тези опции за комерсиални цели. + Изглежда, че вашето устройство не може да получава известия. +\n +\nИзвестията са необходими за да откриете нов акаунт чрез това приложение. За да продължите, моля посетете нашата страница: + Използвайте вашия &appName; акаунт + Моля потвърдете кода на вашата държава и въведете вашия телефонен номер + Моля въведете вашето &appName; потребителско име и парола + Вписване + Завършване + Използване на SIP акаунт + Моля въведете потребителско име и парола на вашия SIP домейн + Моля въведете потребителско име, имейл адрес и парола за вашия &appName; акаунт + Вашия акаунт беше създаден успешно. Моля проверете пощенската си кутия за да потвърдите регистрацията: + Използвайте потребителско име (незадължително) + До вашия телефонен номер бе изпратен SMS с код за потвърждение: + Вие ще свържете вашия телефонен номер с следното потребителско име: + Вашия акаунт все още не е активиран, моля натиснете на връзката в съдържанието на получения от вас имейл + Акаунт по подразбиране + Основен акаунт + Моля въведете парола за достъп до настройките + Натиснете за да започне калибриране за анулиране на ехо + Не е доловено ехо + Адаптивен контрол + Промените ще се отразят при следващото обаждане + Промените ще се отразят при следващото обаждане + Насочете звука към bluetooth устройството, ако има такова + Усилване на микрофона + (в децибели) + Започване на видео обаждане + Предпочитан размер за видео + Предварително зададени настройки за видео + Ограничаване на ширината + в килобита/сек + Използвайте мелодията на устройството + Задължително шифроване на медията + Изисква някои допълнителни разрешения + Моля дайте разрешение за да се наслагват известията + Изпращане на извън обхватни DTMF (SIP INFO) + Изпращане на обхватни DTMF (RFC 2833) + Обаждането ще започне автоматично ако се стартира от друго приложение + Пренасочване на отказаните обаждания към URI за гласова поща + Поставяне на обаждането в пауза при загуба на фокус + Премахнатите известия да се маркират като прочетени + URL на сървър за споделяне + Не редактирайте ако не сте сигурни! + Автоматично изтегляне на входящи файлове + Ако са по-малки от посочения размер + Автоматичното изтегляне на входящи файлове не важи за файловете в краткотрайни съобщения + Ще се замести от преки пътища до контакти, ако е включено + Настройки за известия на Андроид + Автоматично изтегляне на входящи гласови съобщения + Позволяване на IPv6 + Абониране към списъка с приятели + Добавяне на информационни връзки от &appName; контактите към тези на Андроид + Винаги да се пита в кой акаунт да се съхраняват нови контакти + Анонимен + База на търсене + В секунди + Атрибути на име + Други + Логове за отстраняване на грешки + Стартиране заедно с устройството + Промените ще се приложат при следващото стартиране + Настройка за спестяване на батерия + Веднъж като се включи е невъзможно да се спре! + Изключване на сигурния режим за UI + Ще се използва един сървър за качване и втори за теглене + Винаги + Пример: john ако акаунта ви е john@sip.example.org + Парола + Паролата се въвежда повторно при промяна на потребителско име или домейн + Свържете вашия акаунт + Не може да се отмени регистрацията + AVPF обикновен RTCP интервал + Префикс за вашата държава + При набиране на номер, добавяне на префикс + Публикуване на данни за присъствие + Вашият акаунт ще бъде изтрит локално. +\nЗа да го заличите за постоянно, посетете страницата на вашият SIP доставчик. + Вашият акаунт ще бъде изтрит локално. +\nЗа да го заличите за постоянно, посетете нашата платформа за управление: + Оформление по подразбиране + &appName; известия за услуга + &appName; известия за пропуснати обаждания + &appName; известия за текстови съобщения + Пропуснато обаждане от %s + За да повишите нивото на сигурност, проверете следните кодове с вашия кореспондент. + Съобщенията при разговори с висока сигурност са шифровани от \"край до край\". Възможно е да се повиши нивото на сигурност на разговора когато участниците извършат автентикация. За да извършите същото, обадете се на вашия контакт и следвайте процеса за автентикация. + Няма приложение за този тип файл + Желаете ли да изтеглите и приложите конфигурация от този адрес\? + Прилагане + Изисква се автентикация + Покажете за какво се използва телефонният ви номер + Включване или изключване на записа на обаждането + Пренасочване на звука през тон колони + Показване или скриване на звуковото меню + Използване на тон колони за звук + Премахване на файловия трансфер + Контакта е потребител на &appName; + Шифроване от \"край до край\" е включено + Към крайната стъпка при създаване на стая + Изпращане на съобщение + Показване или скриване на устройствата на участниците + \ 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 583bf047e..e8640b3cf 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -2,7 +2,7 @@ ]> - + Kopírovat adresu schůzky Okamžité zahájení volání Volání se spustí automaticky, pokud je spuštěno z jiné aplikace @@ -107,7 +107,7 @@ SIP adresa SIP adresy Zrušit - + den dny dní @@ -670,7 +670,7 @@ Zakázáno 1 hodina 1 týden - + %s píše… %s píší… %s píše… @@ -799,4 +799,25 @@ Nerušit Pryč Softwarové potlačení ozvěny + Vytvoření účtu pomocí e-mailu: + Váš účet ještě nebyl aktivován, klikněte prosím na odkaz, který jste obdrželi e-mailem + Vypnout bundle režim + Zdá se, že vaše zařízení nemůže přijímat push notifikace. +\n +\nProtože je nyní vyžadujeme pro vytvoření účtu, nebudete si moci vytvořit účet v aplikaci, ale můžete si ho vytvořit na našich webových stránkách: + Připojení se nezdařilo, protože přihlašovací údaje chybí nebo jsou neplatné pro účet +\n%s. +\n +\nMůžete znovu zadat heslo nebo zkontrolovat konfiguraci účtu v nastavení. + Přepnutí viditelnosti nástroje pro výběr emoji + Vyžadováno ověření + Potvrdit + Povolení vyžadované pro zobrazení příchozích hovorů zatím nebylo uděleno + Zobrazit + Špatná mezinárodní předvolba + Ano + Ne + Chystáte se vytvořit nový kontakt bez SIP URI ani telefonního čísla, a proto nebude viditelný v seznamu &appName;. +\n +\nChcete jej přesto vytvořit\? \ 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 2478ffe60..ea9be675e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -781,10 +781,19 @@ Valider Le mot de passe est invalide ! Désactiver le mode bundle - Pour créer un compte avec votre email : Votre périphérique ne semble pas supporter les notifications \'push\'.\n\nVous ne pourrez donc pas créer des comptes dans l\'application mais vous pouvez toujours le faire sur notre site internet : Votre compte n\'est pas activé, veuillez cliquer sur le lien que vous avez reçu par courriel + Authentification requise + La connexion a échouée car l\'authentification est manquante ou invalide pour le compte\n%s.\n\nVous pouvez fournir le mot de passe à nouveau ou vérifier la configuration de votre compte dans les paramètres. + Confirmer + Change la visibilité du selectionneur d\'emoji + Permission requise pour afficher les appels entrant non accordée + Afficher + Préfixe international inconnu + Vous êtes sur le point de créer un nouveau contact sans adresse SIP ni numéro de téléphone, il ne sera donc pas visible dans &appName;.\n\nVoulez-vous le créer quand même ? + Oui + Non %s a réagi par %s à : %s %s réaction %s réactions diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 7dd943381..4f1c4fb6b 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -582,4 +582,25 @@ Segítsen nekünk a &appName; fordításában Megértettem Egyes szolgáltatásokhoz egy &appName; fiók kellene, például csoportos üzeneteket vagy elmúló üzeneteket.\n\nEzek a szolgáltatások el vannak rejtve, ha külső SIP-fiókkal regisztrál.\n\nHa engedélyezni szeretné egy kereskedelmi projektben, vegye fel velünk a kapcsolatot. + Ez a Névjegy nem törölhető + A hívás továbbítva + A hívás nem átirányítható + Értesítések engedélyezése + Kérem várjon a letöltéssel amíg az előző befejeződik + A hangüzenetet rögzítettük + Hangüzenet + Elérhető + A mai napon elérhető + Tegnap elérhető + Elrhető a + Elfoglalt + Ne zavarj + A felvétel exportálása a … + Értekezlet + Értekezlet ütemezése + Csoportos hívás indítása + Nincs értekezlet hívás az ön előzményeiben + További találatok érhetők el, finomítsa a keresést + Menj a Névjegyhez + Értesítések letiltása \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 7f3f0ab69..54fc594b9 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -792,5 +792,20 @@ このデバイスではプッシュ通知を受信することができません。 \n \nアカウント作成時にプッシュ通知の受信が必須となったため、このアプリ内ではアカウントを作成することができませんが、当社のWebサイト上で作成することができます: + 認証が必要です + 認証 + 認証の失敗、もしくは無効なアカウントのため +\n%s の接続に失敗しました。 +\n +\n再度パスワードを入力するか、設定からアカウントの構成を確認することができます。 アカウントがアクティベートされていません。受信したEメールのリンクを開いてください + 間違った国際プレフィックス + 着信を表示するために必要な権限がまだ付与されていません + 表示 + 絵文字ピッカーの表示を切り替える + Yes + No + SIP URIも電話番号もない新しい連絡先を作成しようとしており、&appName;には表示されません。 +\n +\n本当に作成しますか? \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 7276f38cc..a4a628afe 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -138,7 +138,14 @@ Meer resultaten zijn beschikbaar, verfijn jouw zoekopdracht Beveiligingsniveau verminderd door %s SIP adressen - Email + E-mail UDP Hallo, doe mee met &appName;! Je kan gratis downloaden op %s + Account aanmaken + Antwoord + Doorsturen + Verwijderen + %s: + Downloaden + Gebruikersnaam (optioneel) \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a6b3daec9..4d83b60ac 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1,2 +1,12 @@ - \ No newline at end of file + + Nieodebrane połączenie + Nieodebrane połączenie od %s + %d nieodebranych połączeń + Nie pokazuj ponownie + Anuluj + Usuń + OK + Akceptuj + Odrzuć + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 081ea0ab9..e509668d7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -608,7 +608,7 @@ Этот звонок записывается. Звонок был приостановлен удалённо. Направить звук на устройство Bluetooth, если таковое имеется - Получать уведомления, когда звонок записывается вашим корреспондентом + Получать уведомления, когда звонок записывается вашим собеседником Метод аутентификации Анонимный Авто @@ -740,4 +740,86 @@ Участники (%d) Начать групповой звонок Входящий групповой звонок - + Перейти к контакту + Отключить уведомления + Отправить обновление через &appName; + Отправить обновление по электронной почте + Шифрование медиа: + Алгоритм согласования ключей: + Ваша учетная запись еще не активирована, пожалуйста, перейдите по ссылке, которую вы получили по электронной почте + Пожалуйста, введите свой пароль ниже, чтобы получить доступ к настройкам + Неверный пароль! + URL сервера ключей шифрования E2E + Ваша учетная запись будет удалена только локально. +\nЧтобы удалить её навсегда, перейдите на веб-сайт вашего SIP-провайдера. + Вы хотите загрузить и применить конфигурацию с этого URL-адреса\? + Требуется аутентификация + Подтвердить + Разрешение, необходимое для отображения входящих вызовов, еще не предоставлено + Уведомления для этого разговора отключены + Пользователь в сети + Завершенных встреч пока нет. + Хотите начать групповой звонок\? +\nВсе участники этой группы получат приглашение присоединиться к встрече. + Похоже, ваше устройство не может получать push-уведомления. +\n +\nПоскольку теперь они требуются для процесса создания учетной записи, вы не сможете создать учетную запись внутри приложения, но вы можете создать ее на нашем веб-сайте: + Ошибка подключения, так как аутентификация отсутствует или недействительна для учетной записи +\n%s. +\n +\nВы можете ввести пароль еще раз или проверить конфигурацию своей учетной записи в настройках. + Сопровождаемый перевод + Постквантовый ZRTP + Отключить режим объединения + Чтобы создать учетную запись с помощью электронной почты: + Для повышения уровня безопасности вы можете уточнить следующие коды у своего собеседника. + %s: + Переключить видимость выбора эмодзи + Позже + Верно + Экспорт записи + Показать + Приложение к собранию + Пользователь не в сети + Пользователь просит не беспокоить + Непредвиденная ошибка… + Включить уведомления + Вы получили голосовое сообщение + Голосовое сообщение + Онлайн + Онлайн сегодня в + Онлайн вчера в + Онлайн в + Далеко + Не беспокоить + Экспорт записи с помощью… + Редактировать встречу + Встреча обновлена: + Встреча отменена: + Начать + Завершенный + Запланированный + Вы хотите удалить эти встречи\? + Конференция отменена организатором + Вы отменили конференцию + Групповой звонок + Снимок сделан: %s + Алгоритм шифрования: + Алгоритм хэширования: + Алгоритм аутентификации: + Алгоритм SAS: + Пароль + Программное подавление эха + Подтвердить + Вы хотите удалить свою учетную запись\? + Ваша учетная запись будет удалена только локально. +\nЧтобы удалить её навсегда, перейдите на нашу платформу управления учетными записями: + Опубликовать информацию о присутствии + Применить + Неправильный международный префикс + Да + Нет + Вы собираетесь создать новый контакт без SIP URI и номера телефона, поэтому он не будет отображаться в &appName;. +\n +\nВы все равно хотите его создать\? + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 1da9bf070..fdb67c706 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -2,7 +2,7 @@ ]> - + 传输 UDP 用户数据报协议 TLS安全传输层协议 @@ -351,9 +351,6 @@ 写入联系人权限被拒绝,无法编辑联系人 您要删除这个联系人吗?\n它也将会从您的设备通讯录中删除 您确定要删除这些联系人吗?\n他们将会从您的设备通讯录中删除 - - \@string/history_delete_one_dialog - 选择保存联系人的位置 本地存储 输入电话号码或地址 @@ -503,17 +500,11 @@ 是否要在此聊天室转发信息? 短暂信息 群组创建失败 - - \@string/chat_room_delete_one_dialog - 对方要求启用视讯 \n如果将您的帐户关联您的电话号码,您的朋友们可以更容易的找到你。\n\n您在通讯录里可以查看谁在使用&appName;,他们也同样可以通过&appName;联系您。\n 请输入您的用户名与密码和您的SIP域名 您的帐户已经创建。请检查邮件以驗證您的帐户: 接收视频分辨率: - - \@string/contact_delete_one_dialog - 撤消通知后标记为已读 发送日志 重启日志 @@ -549,7 +540,7 @@ 信息 按住按钮录制语音信息 您的媒体音量很低,您可能需要提高音量 - + %1$d 未读消息 %1$d 未读消息 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 95435d707..c346eb75e 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -541,7 +541,6 @@ 滑動到畫面底部或第一則未讀訊息 複製會議地址 切換顯示會議詳細資訊 - 您已開啟「閱後即焚」訊息 總是在此程式中開啟 還是可以匯出到第三方應用程式 自動下載收到的錄音檔 @@ -799,6 +798,21 @@ \n \n現在起註冊帳號時就必須使用此功能,您無法直接於程式中註冊帳號,但還是可以在網頁中註冊: 您的帳號尚未啟用,請點擊收到的確認信件中的連結 + 需要驗證 + 由於缺少帳號 %s 的驗證資訊,或是驗證資訊無效,連線失敗。 +\n +\n您可以再次輸入密碼,或是到設定畫面中確認帳號設定。 + 確認 + 您開啟了「閱後即焚」訊息:%s + 切換顯示表情符號選取器 + 尚未授予顯示來電的必要權限 + 顯示 + 國際冠碼錯誤 + + + 您要建立的聯絡人不含 SIP URI 或電話號碼,不會在 &appName; 出現。 +\n +\n還是要建立這位聯絡人嗎? %s 反應 %s 反應 語音訊息 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a2376c008..2a020efb6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,6 +49,8 @@ %d days Share logs link using… + Yes + No Unexpected error… @@ -114,6 +116,7 @@ Store locally This contact can\'t be deleted More results are available, refine your search + You are about to create a new contact without a SIP URI nor a phone number, thus it won\'t be visible in &appName;.\n\nDo you want to create it anyway? Enter a number or an address @@ -438,6 +441,7 @@ Email address is invalid Username has too many characters Account does not exist or password does not match + Wrong international prefix Use your &appName; account @@ -739,7 +743,6 @@ Your account will only be deleted locally.\nTo delete it permanently, go on your SIP provider website. Your account will only be deleted locally.\nTo delete it permanently, go to our account management platform: Disable bundle mode - Default layout @@ -790,6 +793,12 @@ Would you like to try opening it as a plain text file? Do you want to download and apply configuration from this URL? Apply + Authentication needed + Connection failed because authentication is missing or invalid for account \n%s.\n\nYou can provide password again, or check your account configuration in the settings. + Confirm + + Permission required to show incoming calls not granted yet + Show Add a SIP address field @@ -933,4 +942,5 @@ User is online User is offline User is asking not to be disturbed + Toggle emoji picker visibility diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index 01b6e9ff2..43cb1e034 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -1,9 +1,10 @@ - + + @@ -11,8 +12,10 @@ + + diff --git a/build.gradle b/build.gradle index c7c72fda8..6568441f7 100644 --- a/build.gradle +++ b/build.gradle @@ -11,10 +11,10 @@ buildscript { } // for com.github.chrisbanes:PhotoView } dependencies { - classpath 'com.android.tools.build:gradle:8.0.1' + classpath 'com.android.tools.build:gradle:8.1.0' classpath 'com.google.gms:google-services:4.3.15' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.5' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.6' } }