Merge branch 'release/5.1'

This commit is contained in:
Sylvain Berfini 2023-08-21 12:56:48 +02:00
commit ede66c2fa7
133 changed files with 2310 additions and 611 deletions

View file

@ -15,17 +15,19 @@ Group changes to describe their impact on the project, as follows:
### Added ### Added
- Chat messages emoji "reactions" - Chat messages emoji "reactions"
## [5.1.0] - Unreleased ## [5.1.0] - 2023-08-21
### Added ### Added
- Showing short term presence for contacts whom publish it + added setting to disable it (enabled by default for sip.linphone.org accounts) - 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 - Confirmation dialog before removing account
- Attended transfer instead of blind transfer if there is more than 1 call - 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 - Last sent message delivery status (IMDN) icon in chat rooms list
- Added hidden setting to disable video completely - Emoji picker in chat room, and increase size of text if it only contains emojis
- Added hidden setting to prevent adding / editing / removing native contacts - Hidden setting to disable video completely
- Added hidden setting to protect settings access using account password - 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 - SIP URI in call can be selected using long press
- Dialog showing up asking for correct account password in case of failed authentication
### Changed ### Changed
- Switched Account Creator backend from XMLRPC to FlexiAPI, it now requires to be able to receive a push notification - 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 ### Fixed
- Messages not marked as reply in basic chat room if sending more than 1 content - 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 ## [5.0.11] - 2023-05-09

View file

@ -7,16 +7,16 @@ plugins {
} }
def appVersionName = "5.1.0" def appVersionName = "5.1.0"
def appVersionCode = 50090 def appVersionCode = 51000
static def getPackageName() { def packageName = "org.linphone"
return "org.linphone"
}
def firebaseAvailable = new File(projectDir.absolutePath +'/google-services.json').exists() 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 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) { if (firebaseAvailable) {
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
} }
@ -83,13 +83,13 @@ android {
targetCompatibility = 17 targetCompatibility = 17
} }
compileSdkVersion 33 compileSdkVersion 34
defaultConfig { defaultConfig {
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 33 targetSdkVersion 34
versionCode appVersionCode versionCode appVersionCode
versionName "${project.version}" versionName "${project.version}"
applicationId getPackageName() applicationId packageName
} }
applicationVariants.all { variant -> applicationVariants.all { variant ->
@ -101,19 +101,19 @@ android {
if (firebaseAvailable) { if (firebaseAvailable) {
enableFirebaseService = "true" 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 // 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") { 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", variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + packageName + ".provider.sip_address",
linphone_file_provider: getPackageName() + ".fileprovider", linphone_file_provider: packageName + ".fileprovider",
appLabel: "@string/app_name", appLabel: "@string/app_name",
firebaseServiceEnabled: enableFirebaseService, firebaseServiceEnabled: enableFirebaseService]
extractNativeLibs: "false"]
} else { } else {
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address", variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + packageName + ".provider.sip_address",
linphone_file_provider: getPackageName() + ".debug.fileprovider", linphone_file_provider: packageName + ".debug.fileprovider",
appLabel: "@string/app_name_debug", appLabel: "@string/app_name_debug",
firebaseServiceEnabled: enableFirebaseService, firebaseServiceEnabled: enableFirebaseService]
extractNativeLibs: "true"] extractNativeLibs = true
} }
} }
@ -137,9 +137,9 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
resValue "string", "linphone_app_branch", gitBranch.toString().trim() resValue "string", "linphone_app_branch", gitBranch.toString().trim()
resValue "string", "sync_account_type", getPackageName() + ".sync" resValue "string", "sync_account_type", packageName + ".sync"
resValue "string", "file_provider", getPackageName() + ".fileprovider" resValue "string", "file_provider", packageName + ".fileprovider"
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address" resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + packageName + ".provider.sip_address"
if (!firebaseAvailable) { if (!firebaseAvailable) {
resValue "string", "gcm_defaultSenderId", "none" resValue "string", "gcm_defaultSenderId", "none"
@ -169,9 +169,9 @@ android {
jniDebuggable true jniDebuggable true
resValue "string", "linphone_app_branch", gitBranch.toString().trim() resValue "string", "linphone_app_branch", gitBranch.toString().trim()
resValue "string", "sync_account_type", getPackageName() + ".sync" resValue "string", "sync_account_type", packageName + ".sync"
resValue "string", "file_provider", getPackageName() + ".debug.fileprovider" resValue "string", "file_provider", packageName + ".debug.fileprovider"
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address" resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + packageName + ".provider.sip_address"
resValue "bool", "crashlytics_enabled", crashlyticsAvailable.toString() resValue "bool", "crashlytics_enabled", crashlyticsAvailable.toString()
if (!firebaseAvailable) { if (!firebaseAvailable) {
@ -193,28 +193,35 @@ android {
} }
namespace 'org.linphone' namespace 'org.linphone'
packagingOptions {
jniLibs {
useLegacyPackaging extractNativeLibs
}
}
} }
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.6.1' 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.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.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation 'androidx.media:media:1.6.0' implementation 'androidx.media:media:1.6.0'
implementation "androidx.security:security-crypto-ktx:1.1.0-alpha06" 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-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0" implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "androidx.gridlayout:gridlayout:1.0.0" 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' implementation 'androidx.drawerlayout:drawerlayout:1.2.0'
// https://github.com/material-components/material-components-android/blob/master/LICENSE Apache v2.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' implementation 'com.google.android.flexbox:flexbox:3.0.0'
// https://github.com/coil-kt/coil/blob/main/LICENSE.txt Apache v2.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:$coil_version")
implementation("io.coil-kt:coil-gif:$coil_version") implementation("io.coil-kt:coil-gif:$coil_version")
implementation("io.coil-kt:coil-svg:$coil_version") implementation("io.coil-kt:coil-svg:$coil_version")
@ -247,7 +254,7 @@ dependencies {
implementation 'org.linphone:linphone-sdk-android:5.3+' implementation 'org.linphone:linphone-sdk-android:5.3+'
// Only enable leak canary prior to release // 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) { task generateContactsXml(type: Copy) {
@ -257,7 +264,7 @@ task generateContactsXml(type: Copy) {
filter { filter {
line -> line line -> line
.replaceAll('%%AUTO_GENERATED%%', 'This file has been automatically generated, do not edit or commit !') .replaceAll('%%AUTO_GENERATED%%', 'This file has been automatically generated, do not edit or commit !')
.replaceAll('%%PACKAGE_NAME%%', getPackageName()) .replaceAll('%%PACKAGE_NAME%%', packageName)
} }
} }

View file

@ -54,6 +54,12 @@
<!-- Needed for foreground service <!-- Needed for foreground service
(https://developer.android.com/guide/components/foreground-services) --> (https://developer.android.com/guide/components/foreground-services) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- Needed for Android 14
https://developer.android.com/about/versions/14/behavior-changes-14#fgs-types -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application <application
android:name=".LinphoneApplication" android:name=".LinphoneApplication"
@ -63,7 +69,6 @@
android:label="${appLabel}" android:label="${appLabel}"
android:localeConfig="@xml/locales_config" android:localeConfig="@xml/locales_config"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:extractNativeLibs="${extractNativeLibs}"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:allowNativeHeapPointerTagging="false"> android:allowNativeHeapPointerTagging="false">
@ -121,6 +126,7 @@
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.DIAL" /> <action android:name="android.intent.action.DIAL" />
<action android:name="android.intent.action.CALL" /> <action android:name="android.intent.action.CALL" />
<action android:name="android.intent.action.CALL_BUTTON" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tel" /> <data android:scheme="tel" />
@ -128,12 +134,6 @@
<data android:scheme="sips" /> <data android:scheme="sips" />
<data android:scheme="linphone" /> <data android:scheme="linphone" />
<data android:scheme="sip-linphone" /> <data android:scheme="sip-linphone" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="linphone-config" /> <data android:scheme="linphone-config" />
</intent-filter> </intent-filter>
@ -169,7 +169,14 @@
<service <service
android:name=".core.CoreService" android:name=".core.CoreService"
android:exported="false" android:exported="false"
android:foregroundServiceType="phoneCall|camera|microphone" android:foregroundServiceType="phoneCall|camera|microphone|dataSync"
android:stopWithTask="false"
android:label="@string/app_name" />
<service
android:name="org.linphone.core.tools.service.PushService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:stopWithTask="false" android:stopWithTask="false"
android:label="@string/app_name" /> android:label="@string/app_name" />

View file

@ -80,7 +80,8 @@ class LinphoneApplication : Application(), ImageLoaderFactory {
context: Context, context: Context,
pushReceived: Boolean = false, pushReceived: Boolean = false,
service: CoreService? = null, service: CoreService? = null,
useAutoStartDescription: Boolean = false useAutoStartDescription: Boolean = false,
skipCoreStart: Boolean = false
): Boolean { ): Boolean {
if (::coreContext.isInitialized && !coreContext.stopped) { if (::coreContext.isInitialized && !coreContext.stopped) {
Log.d("[Application] Skipping Core creation (push received? $pushReceived)") Log.d("[Application] Skipping Core creation (push received? $pushReceived)")
@ -96,7 +97,9 @@ class LinphoneApplication : Application(), ImageLoaderFactory {
service, service,
useAutoStartDescription useAutoStartDescription
) )
if (!skipCoreStart) {
coreContext.start() coreContext.start()
}
return true return true
} }

View file

@ -83,7 +83,9 @@ abstract class GenericFragment<T : ViewDataBinding> : Fragment() {
} }
} }
} catch (ise: IllegalStateException) { } 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<T : ViewDataBinding> : Fragment() {
try { try {
requireActivity().onBackPressedDispatcher.onBackPressed() requireActivity().onBackPressedDispatcher.onBackPressed()
} catch (ise: IllegalStateException) { } 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()
} }
} }

View file

@ -75,6 +75,10 @@ class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragment
startActivity(intent) startActivity(intent)
} }
viewModel.prefix.observe(viewLifecycleOwner) { internationalPrefix ->
viewModel.getCountryNameFromPrefix(internationalPrefix)
}
viewModel.goToSmsValidationEvent.observe( viewModel.goToSmsValidationEvent.observe(
viewLifecycleOwner viewLifecycleOwner
) { ) {

View file

@ -29,7 +29,7 @@ import org.linphone.activities.assistant.adapters.CountryPickerAdapter
import org.linphone.core.DialPlan import org.linphone.core.DialPlan
import org.linphone.databinding.AssistantCountryPickerFragmentBinding import org.linphone.databinding.AssistantCountryPickerFragmentBinding
class CountryPickerFragment() : DialogFragment() { class CountryPickerFragment : DialogFragment() {
private var _binding: AssistantCountryPickerFragmentBinding? = null private var _binding: AssistantCountryPickerFragmentBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var adapter: CountryPickerAdapter private lateinit var adapter: CountryPickerAdapter

View file

@ -62,6 +62,10 @@ class PhoneAccountCreationFragment :
countryPickerFragment.show(childFragmentManager, "CountryPicker") countryPickerFragment.show(childFragmentManager, "CountryPicker")
} }
viewModel.prefix.observe(viewLifecycleOwner) { internationalPrefix ->
viewModel.getCountryNameFromPrefix(internationalPrefix)
}
viewModel.goToSmsValidationEvent.observe( viewModel.goToSmsValidationEvent.observe(
viewLifecycleOwner viewLifecycleOwner
) { ) {

View file

@ -73,6 +73,10 @@ class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountL
countryPickerFragment.show(childFragmentManager, "CountryPicker") countryPickerFragment.show(childFragmentManager, "CountryPicker")
} }
viewModel.prefix.observe(viewLifecycleOwner) { internationalPrefix ->
viewModel.getCountryNameFromPrefix(internationalPrefix)
}
viewModel.goToSmsValidationEvent.observe( viewModel.goToSmsValidationEvent.observe(
viewLifecycleOwner viewLifecycleOwner
) { ) {

View file

@ -34,6 +34,7 @@ import org.linphone.activities.assistant.viewmodels.PhoneAccountValidationViewMo
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
import org.linphone.activities.navigateToAccountSettings import org.linphone.activities.navigateToAccountSettings
import org.linphone.activities.navigateToEchoCancellerCalibration import org.linphone.activities.navigateToEchoCancellerCalibration
import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantPhoneAccountValidationFragmentBinding import org.linphone.databinding.AssistantPhoneAccountValidationFragmentBinding
@ -113,7 +114,7 @@ class PhoneAccountValidationFragment : GenericFragment<AssistantPhoneAccountVali
"[Assistant] [Phone Account Validation] Found 4 digits as primary clip in clipboard, using it and clear it" "[Assistant] [Phone Account Validation] Found 4 digits as primary clip in clipboard, using it and clear it"
) )
viewModel.code.value = clip viewModel.code.value = clip
clipboard.clearPrimaryClip() Compatibility.clearClipboard(clipboard)
} }
} }
} }

View file

@ -21,7 +21,6 @@
package org.linphone.activities.assistant.viewmodels package org.linphone.activities.assistant.viewmodels
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.*
import org.linphone.activities.assistant.fragments.CountryPickerFragment import org.linphone.activities.assistant.fragments.CountryPickerFragment
import org.linphone.core.AccountCreator import org.linphone.core.AccountCreator
import org.linphone.core.DialPlan import org.linphone.core.DialPlan
@ -33,6 +32,7 @@ abstract class AbstractPhoneViewModel(accountCreator: AccountCreator) :
CountryPickerFragment.CountryPickedListener { CountryPickerFragment.CountryPickedListener {
val prefix = MutableLiveData<String>() val prefix = MutableLiveData<String>()
val prefixError = MutableLiveData<String>()
val phoneNumber = MutableLiveData<String>() val phoneNumber = MutableLiveData<String>()
val phoneNumberError = MutableLiveData<String>() val phoneNumberError = MutableLiveData<String>()
@ -49,7 +49,10 @@ abstract class AbstractPhoneViewModel(accountCreator: AccountCreator) :
} }
fun isPhoneNumberOk(): Boolean { 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?) { 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()) { if (!prefix.isNullOrEmpty()) {
val countryCode = if (prefix.first() == '+') prefix.substring(1) else prefix val countryCode = if (prefix.first() == '+') prefix.substring(1) else prefix
val dialPlan = PhoneNumberUtils.getDialPlanFromCountryCallingPrefix(countryCode) val dialPlan = PhoneNumberUtils.getDialPlanFromCountryCallingPrefix(countryCode)

View file

@ -46,7 +46,7 @@ class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewM
val password = MutableLiveData<String>() val password = MutableLiveData<String>()
val passwordError = MutableLiveData<String>() val passwordError = MutableLiveData<String>()
val loginEnabled: MediatorLiveData<Boolean> = MediatorLiveData() val loginEnabled = MediatorLiveData<Boolean>()
val waitForServerAnswer = MutableLiveData<Boolean>() val waitForServerAnswer = MutableLiveData<Boolean>()
@ -140,6 +140,9 @@ class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewM
loginEnabled.addSource(phoneNumberError) { loginEnabled.addSource(phoneNumberError) {
loginEnabled.value = isLoginButtonEnabled() loginEnabled.value = isLoginButtonEnabled()
} }
loginEnabled.addSource(prefixError) {
loginEnabled.value = isLoginButtonEnabled()
}
} }
override fun onCleared() { override fun onCleared() {
@ -149,6 +152,7 @@ class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewM
override fun onFlexiApiTokenReceived() { override fun onFlexiApiTokenReceived() {
Log.i("[Assistant] [Account Login] Using FlexiAPI auth token [${accountCreator.token}]") Log.i("[Assistant] [Account Login] Using FlexiAPI auth token [${accountCreator.token}]")
waitForServerAnswer.value = false
loginWithPhoneNumber() loginWithPhoneNumber()
} }
@ -161,10 +165,19 @@ class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewM
fun removeInvalidProxyConfig() { fun removeInvalidProxyConfig() {
val account = accountToCheck val account = accountToCheck
account ?: return account ?: return
val core = coreContext.core
val authInfo = account.findAuthInfo() val authInfo = account.findAuthInfo()
if (authInfo != null) coreContext.core.removeAuthInfo(authInfo) if (authInfo != null) core.removeAuthInfo(authInfo)
coreContext.core.removeAccount(account) core.removeAccount(account)
accountToCheck = null 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() { fun continueEvenIfInvalidCredentials() {

View file

@ -51,16 +51,16 @@ class EchoCancellerCalibrationViewModel : ViewModel() {
coreContext.core.removeListener(listener) coreContext.core.removeListener(listener)
when (status) { when (status) {
EcCalibratorStatus.DoneNoEcho -> { EcCalibratorStatus.DoneNoEcho -> {
Log.i("[Echo Canceller Calibration] Done, no echo") Log.i("[Assistant] [Echo Canceller Calibration] Done, no echo")
} }
EcCalibratorStatus.Done -> { 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 -> { EcCalibratorStatus.Failed -> {
Log.w("[Echo Canceller Calibration] Failed") Log.w("[Assistant] [Echo Canceller Calibration] Failed")
} }
EcCalibratorStatus.InProgress -> { EcCalibratorStatus.InProgress -> {
Log.i("[Echo Canceller Calibration] In progress") Log.i("[Assistant] [Echo Canceller Calibration] In progress")
} }
} }
echoCalibrationTerminated.value = Event(true) echoCalibrationTerminated.value = Event(true)

View file

@ -72,7 +72,7 @@ class EmailAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPu
status: AccountCreator.Status, status: AccountCreator.Status,
response: String? response: String?
) { ) {
Log.i("[Account Creation] onIsAccountExist status is $status") Log.i("[Assistant] [Account Creation] onIsAccountExist status is $status")
when (status) { when (status) {
AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> { AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> {
waitForServerAnswer.value = false waitForServerAnswer.value = false
@ -99,7 +99,7 @@ class EmailAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPu
status: AccountCreator.Status, status: AccountCreator.Status,
response: String? response: String?
) { ) {
Log.i("[Account Creation] onCreateAccount status is $status") Log.i("[Assistant] [Account Creation] onCreateAccount status is $status")
waitForServerAnswer.value = false waitForServerAnswer.value = false
when (status) { when (status) {
@ -149,11 +149,11 @@ class EmailAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPu
} }
override fun onFlexiApiTokenReceived() { 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 waitForServerAnswer.value = true
val status = accountCreator.isAccountExist 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) { if (status != AccountCreator.Status.RequestOk) {
waitForServerAnswer.value = false waitForServerAnswer.value = false
onErrorEvent.value = Event("Error: ${status.name}") onErrorEvent.value = Event("Error: ${status.name}")
@ -161,7 +161,7 @@ class EmailAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPu
} }
override fun onFlexiApiTokenRequestError() { 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 waitForServerAnswer.value = false
onErrorEvent.value = Event("Error: Failed to get an auth token from account manager server") 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() val token = accountCreator.token.orEmpty()
if (token.isNotEmpty()) { if (token.isNotEmpty()) {
Log.i( 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() onFlexiApiTokenReceived()
} else { } 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 waitForServerAnswer.value = true
requestFlexiApiToken() requestFlexiApiToken()
} }

View file

@ -57,7 +57,7 @@ class EmailAccountValidationViewModel(val accountCreator: AccountCreator) : View
status: AccountCreator.Status, status: AccountCreator.Status,
response: String? response: String?
) { ) {
Log.i("[Account Validation] onIsAccountActivated status is $status") Log.i("[Assistant] [Account Validation] onIsAccountActivated status is $status")
waitForServerAnswer.value = false waitForServerAnswer.value = false
when (status) { when (status) {

View file

@ -108,10 +108,19 @@ class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewMo
fun removeInvalidProxyConfig() { fun removeInvalidProxyConfig() {
val account = accountToCheck val account = accountToCheck
account ?: return account ?: return
val core = coreContext.core
val authInfo = account.findAuthInfo() val authInfo = account.findAuthInfo()
if (authInfo != null) coreContext.core.removeAuthInfo(authInfo) if (authInfo != null) core.removeAuthInfo(authInfo)
coreContext.core.removeAccount(account) core.removeAccount(account)
accountToCheck = null 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() { fun continueEvenIfInvalidCredentials() {
@ -143,6 +152,8 @@ class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewMo
} }
private fun isLoginButtonEnabled(): Boolean { 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()
} }
} }

View file

@ -68,7 +68,7 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
status: AccountCreator.Status, status: AccountCreator.Status,
response: String? response: String?
) { ) {
Log.i("[Phone Account Creation] onIsAccountExist status is $status") Log.i("[Assistant] [Phone Account Creation] onIsAccountExist status is $status")
when (status) { when (status) {
AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> { AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> {
waitForServerAnswer.value = false waitForServerAnswer.value = false
@ -92,7 +92,7 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
status: AccountCreator.Status, status: AccountCreator.Status,
response: String? response: String?
) { ) {
Log.i("[Phone Account Creation] onIsAliasUsed status is $status") Log.i("[Assistant] [Phone Account Creation] onIsAliasUsed status is $status")
when (status) { when (status) {
AccountCreator.Status.AliasExist -> { AccountCreator.Status.AliasExist -> {
waitForServerAnswer.value = false waitForServerAnswer.value = false
@ -114,7 +114,9 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
} }
AccountCreator.Status.AliasNotExist -> { AccountCreator.Status.AliasNotExist -> {
val createAccountStatus = creator.createAccount() 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) { if (createAccountStatus != AccountCreator.Status.RequestOk) {
waitForServerAnswer.value = false waitForServerAnswer.value = false
onErrorEvent.value = Event("Error: ${status.name}") onErrorEvent.value = Event("Error: ${status.name}")
@ -132,7 +134,7 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
status: AccountCreator.Status, status: AccountCreator.Status,
response: String? response: String?
) { ) {
Log.i("[Phone Account Creation] onCreateAccount status is $status") Log.i("[Assistant] [Phone Account Creation] onCreateAccount status is $status")
waitForServerAnswer.value = false waitForServerAnswer.value = false
when (status) { when (status) {
AccountCreator.Status.AccountCreated -> { AccountCreator.Status.AccountCreated -> {
@ -173,6 +175,9 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
createEnabled.addSource(phoneNumberError) { createEnabled.addSource(phoneNumberError) {
createEnabled.value = isCreateButtonEnabled() createEnabled.value = isCreateButtonEnabled()
} }
createEnabled.addSource(prefixError) {
createEnabled.value = isCreateButtonEnabled()
}
} }
override fun onCleared() { override fun onCleared() {
@ -181,9 +186,22 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
} }
override fun onFlexiApiTokenReceived() { 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.displayName = displayName.value
val result = AccountCreator.PhoneNumberStatus.fromInt(
accountCreator.setPhoneNumber(phoneNumber.value, prefix.value) 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) { if (useUsername.value == true) {
accountCreator.username = username.value accountCreator.username = username.value
@ -199,14 +217,14 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
} }
override fun onFlexiApiTokenRequestError() { 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 waitForServerAnswer.value = false
onErrorEvent.value = Event("Error: Failed to get an auth token from account manager server") onErrorEvent.value = Event("Error: Failed to get an auth token from account manager server")
} }
private fun checkUsername() { private fun checkUsername() {
val status = accountCreator.isAccountExist 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) { if (status != AccountCreator.Status.RequestOk) {
waitForServerAnswer.value = false waitForServerAnswer.value = false
onErrorEvent.value = Event("Error: ${status.name}") onErrorEvent.value = Event("Error: ${status.name}")
@ -215,7 +233,7 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
private fun checkPhoneNumber() { private fun checkPhoneNumber() {
val status = accountCreator.isAliasUsed 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) { if (status != AccountCreator.Status.RequestOk) {
waitForServerAnswer.value = false waitForServerAnswer.value = false
onErrorEvent.value = Event("Error: ${status.name}") onErrorEvent.value = Event("Error: ${status.name}")
@ -226,11 +244,11 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
val token = accountCreator.token.orEmpty() val token = accountCreator.token.orEmpty()
if (token.isNotEmpty()) { if (token.isNotEmpty()) {
Log.i( 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() onFlexiApiTokenReceived()
} else { } 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 waitForServerAnswer.value = true
requestFlexiApiToken() requestFlexiApiToken()
} }

View file

@ -60,12 +60,12 @@ class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPho
status: AccountCreator.Status, status: AccountCreator.Status,
response: String? response: String?
) { ) {
Log.i("[Phone Account Linking] onIsAliasUsed status is $status") Log.i("[Assistant] [Phone Account Linking] onIsAliasUsed status is $status")
when (status) { when (status) {
AccountCreator.Status.AliasNotExist -> { AccountCreator.Status.AliasNotExist -> {
if (creator.linkAccount() != AccountCreator.Status.RequestOk) { 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 waitForServerAnswer.value = false
onErrorEvent.value = Event("Error: ${status.name}") onErrorEvent.value = Event("Error: ${status.name}")
} }
@ -86,7 +86,7 @@ class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPho
status: AccountCreator.Status, status: AccountCreator.Status,
response: String? response: String?
) { ) {
Log.i("[Phone Account Linking] onLinkAccount status is $status") Log.i("[Assistant] [Phone Account Linking] onLinkAccount status is $status")
waitForServerAnswer.value = false waitForServerAnswer.value = false
when (status) { when (status) {
@ -113,6 +113,9 @@ class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPho
linkEnabled.addSource(phoneNumberError) { linkEnabled.addSource(phoneNumberError) {
linkEnabled.value = isLinkButtonEnabled() linkEnabled.value = isLinkButtonEnabled()
} }
linkEnabled.addSource(prefixError) {
linkEnabled.value = isLinkButtonEnabled()
}
} }
override fun onCleared() { override fun onCleared() {
@ -123,10 +126,10 @@ class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPho
override fun onFlexiApiTokenReceived() { override fun onFlexiApiTokenReceived() {
accountCreator.setPhoneNumber(phoneNumber.value, prefix.value) accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
accountCreator.username = username.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 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) { if (status != AccountCreator.Status.RequestOk) {
waitForServerAnswer.value = false waitForServerAnswer.value = false
onErrorEvent.value = Event("Error: ${status.name}") onErrorEvent.value = Event("Error: ${status.name}")
@ -134,12 +137,12 @@ class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPho
} }
override fun onFlexiApiTokenRequestError() { 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 waitForServerAnswer.value = false
} }
fun link() { 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 waitForServerAnswer.value = true
requestFlexiApiToken() requestFlexiApiToken()
} }

View file

@ -34,7 +34,7 @@ class QrCodeViewModel : ViewModel() {
private val listener = object : CoreListenerStub() { private val listener = object : CoreListenerStub() {
override fun onQrcodeFound(core: Core, result: String?) { 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)) if (result != null) qrCodeFoundEvent.postValue(Event(result))
} }
} }
@ -54,7 +54,7 @@ class QrCodeViewModel : ViewModel() {
for (camera in coreContext.core.videoDevicesList) { for (camera in coreContext.core.videoDevicesList) {
if (camera.contains("Back")) { 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 coreContext.core.videoDevice = camera
return return
} }
@ -62,7 +62,7 @@ class QrCodeViewModel : ViewModel() {
val first = coreContext.core.videoDevicesList.firstOrNull() val first = coreContext.core.videoDevicesList.firstOrNull()
if (first != null) { 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 coreContext.core.videoDevice = first
} }
} }

View file

@ -77,7 +77,7 @@ class RemoteProvisioningViewModel : ViewModel() {
fun fetchAndApply() { fun fetchAndApply() {
val url = urlToFetch.value.orEmpty() val url = urlToFetch.value.orEmpty()
coreContext.core.provisioningUri = url 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 fetchInProgress.value = true
coreContext.core.stop() coreContext.core.stop()
coreContext.core.start() coreContext.core.start()

View file

@ -35,6 +35,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.doOnAttach import androidx.core.view.doOnAttach
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController import androidx.navigation.NavController
@ -58,6 +59,10 @@ import org.linphone.activities.main.viewmodels.SharedMainViewModel
import org.linphone.activities.navigateToDialer import org.linphone.activities.navigateToDialer
import org.linphone.compatibility.Compatibility import org.linphone.compatibility.Compatibility
import org.linphone.contact.ContactsUpdatedListenerStub 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.CorePreferences
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.databinding.MainActivityBinding import org.linphone.databinding.MainActivityBinding
@ -108,6 +113,25 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
private var shouldTabsBeVisibleDependingOnDestination = true private var shouldTabsBeVisibleDependingOnDestination = true
private var shouldTabsBeVisibleDueToOrientationAndKeyboard = true private var shouldTabsBeVisibleDueToOrientationAndKeyboard = true
private val authenticationRequestedEvent: MutableLiveData<Event<AuthInfo>> by lazy {
MutableLiveData<Event<AuthInfo>>()
}
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<AppUtils.KeyboardVisibilityListener>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) 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 (coreContext.core.accountList.isEmpty()) {
if (corePreferences.firstStart) { if (corePreferences.firstStart) {
startActivity(Intent(this, AssistantActivity::class.java)) startActivity(Intent(this, AssistantActivity::class.java))
@ -174,9 +206,11 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
coreContext.contactsManager.addListener(listener) coreContext.contactsManager.addListener(listener)
coreContext.core.addListener(coreListener)
} }
override fun onPause() { override fun onPause() {
coreContext.core.removeListener(coreListener)
coreContext.contactsManager.removeListener(listener) coreContext.contactsManager.removeListener(listener)
super.onPause() super.onPause()
} }
@ -205,13 +239,17 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
registerComponentCallbacks(componentCallbacks) registerComponentCallbacks(componentCallbacks)
findNavController(R.id.nav_host_fragment).addOnDestinationChangedListener(this) findNavController(R.id.nav_host_fragment).addOnDestinationChangedListener(this)
binding.rootCoordinatorLayout.addKeyboardInsetListener { keyboardVisible -> binding.rootCoordinatorLayout.setKeyboardInsetListener { keyboardVisible ->
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
Log.i( Log.i(
"[Main Activity] Keyboard is ${if (keyboardVisible) "visible" else "invisible"}, orientation is ${if (portraitOrientation) "portrait" else "landscape"}" "[Main Activity] Keyboard is ${if (keyboardVisible) "visible" else "invisible"}, orientation is ${if (portraitOrientation) "portrait" else "landscape"}"
) )
shouldTabsBeVisibleDueToOrientationAndKeyboard = !portraitOrientation || !keyboardVisible shouldTabsBeVisibleDueToOrientationAndKeyboard = !portraitOrientation || !keyboardVisible
updateTabsFragmentVisibility() updateTabsFragmentVisibility()
for (listener in keyboardVisibilityListeners) {
listener.onKeyboardVisibilityChanged(keyboardVisible)
}
} }
initOverlay() initOverlay()
@ -246,6 +284,14 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
updateTabsFragmentVisibility() updateTabsFragmentVisibility()
} }
fun addKeyboardVisibilityListener(listener: AppUtils.KeyboardVisibilityListener) {
keyboardVisibilityListeners.add(listener)
}
fun removeKeyboardVisibilityListener(listener: AppUtils.KeyboardVisibilityListener) {
keyboardVisibilityListeners.remove(listener)
}
fun hideKeyboard() { fun hideKeyboard() {
currentFocus?.hideKeyboard() currentFocus?.hideKeyboard()
} }
@ -343,7 +389,12 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
// Prevent this intent to be processed again // Prevent this intent to be processed again
intent.action = null intent.action = null
intent.data = 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) { private fun handleMainIntent(intent: Intent) {
@ -420,6 +471,8 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
} }
} }
addressToCall = addressToCall.replace("%40", "@")
val address = coreContext.core.interpretUrl( val address = coreContext.core.interpretUrl(
addressToCall, addressToCall,
LinphoneUtils.applyInternationalPrefix() LinphoneUtils.applyInternationalPrefix()
@ -630,4 +683,43 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
dialog.show() 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()
}
} }

View file

@ -35,10 +35,11 @@ class ChatMessageAttachmentData(
init { init {
val extension = FileUtils.getExtensionFromFileName(path) val extension = FileUtils.getExtensionFromFileName(path)
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
isImage = FileUtils.isMimeImage(mime) val mimeType = FileUtils.getMimeType(mime)
isVideo = FileUtils.isMimeVideo(mime) isImage = mimeType == FileUtils.MimeType.Image
isAudio = FileUtils.isMimeAudio(mime) isVideo = mimeType == FileUtils.MimeType.Video
isPdf = FileUtils.isMimePdf(mime) isAudio = mimeType == FileUtils.MimeType.Audio
isPdf = mimeType == FileUtils.MimeType.Pdf
} }
fun delete() { fun delete() {

View file

@ -83,7 +83,6 @@ class ChatMessageContentData(
val conferenceDate = MutableLiveData<String>() val conferenceDate = MutableLiveData<String>()
val conferenceTime = MutableLiveData<String>() val conferenceTime = MutableLiveData<String>()
val conferenceDuration = MutableLiveData<String>() val conferenceDuration = MutableLiveData<String>()
var conferenceAddress = MutableLiveData<String>()
val showDuration = MutableLiveData<Boolean>() val showDuration = MutableLiveData<Boolean>()
val isAlone: Boolean val isAlone: Boolean
@ -107,6 +106,8 @@ class ChatMessageContentData(
stopVoiceRecording() stopVoiceRecording()
} }
private var conferenceAddress: String? = null
private fun getContent(): Content { private fun getContent(): Content {
return chatMessage.contents[contentIndex] return chatMessage.contents[contentIndex]
} }
@ -182,7 +183,7 @@ class ChatMessageContentData(
val content = getContent() val content = getContent()
val filePath = content.filePath val filePath = content.filePath
if (content.isFileTransfer) { if (content.isFileTransfer) {
if (filePath == null || filePath.isEmpty()) { if (filePath.isNullOrEmpty()) {
val contentName = content.name val contentName = content.name
if (contentName != null) { if (contentName != null) {
val file = FileUtils.getFileStoragePath(contentName) val file = FileUtils.getFileStoragePath(contentName)
@ -274,18 +275,26 @@ class ChatMessageContentData(
filePath.value = path filePath.value = path
val extension = FileUtils.getExtensionFromFileName(path) val extension = FileUtils.getExtensionFromFileName(path)
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
isImage.value = FileUtils.isMimeImage(mime) val type = when (FileUtils.getMimeType(mime)) {
isVideo.value = FileUtils.isMimeVideo(mime) && !isVoiceRecord FileUtils.MimeType.Image -> {
isAudio.value = FileUtils.isMimeAudio(mime) && !isVoiceRecord isImage.value = true
isPdf.value = FileUtils.isMimePdf(mime) "image"
val type = when { }
isImage.value == true -> "image" FileUtils.MimeType.Video -> {
isVideo.value == true -> "video" isVideo.value = !isVoiceRecord
isAudio.value == true -> "audio" if (isVoiceRecord) "voice recording" else "video"
isPdf.value == true -> "pdf" }
isVoiceRecord -> "voice recording" FileUtils.MimeType.Audio -> {
isConferenceIcs -> "conference invitation" isAudio.value = !isVoiceRecord
else -> "unknown" if (isVoiceRecord) "voice recording" else "audio"
}
FileUtils.MimeType.Pdf -> {
isPdf.value = true
"pdf"
}
else -> {
if (isConferenceIcs) "conference invitation" else "unknown"
}
} }
Log.i( Log.i(
"[Content] Extension for file [$path] is [$extension], deduced type from MIME is [$type]" "[Content] Extension for file [$path] is [$extension], deduced type from MIME is [$type]"
@ -310,23 +319,26 @@ class ChatMessageContentData(
Log.w( Log.w(
"[Content] Found ${if (content.isFile) "file" else "file transfer"} content with empty path..." "[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) { } else if (content.isFileTransfer) {
downloadable.value = true downloadable.value = true
val extension = FileUtils.getExtensionFromFileName(fileName.value!!) val extension = FileUtils.getExtensionFromFileName(fileName.value!!)
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
isImage.value = FileUtils.isMimeImage(mime) when (FileUtils.getMimeType(mime)) {
isVideo.value = FileUtils.isMimeVideo(mime) FileUtils.MimeType.Image -> {
isAudio.value = FileUtils.isMimeAudio(mime) isImage.value = true
isPdf.value = FileUtils.isMimePdf(mime) }
isVoiceRecording.value = false FileUtils.MimeType.Video -> {
isConferenceSchedule.value = false isVideo.value = true
}
FileUtils.MimeType.Audio -> {
isAudio.value = true
}
FileUtils.MimeType.Pdf -> {
isPdf.value = true
}
else -> {}
}
} else if (content.isIcalendar) { } else if (content.isIcalendar) {
Log.i("[Content] Found content with icalendar body") Log.i("[Content] Found content with icalendar body")
isConferenceSchedule.value = true isConferenceSchedule.value = true
@ -342,9 +354,9 @@ class ChatMessageContentData(
val conferenceInfo = Factory.instance().createConferenceInfoFromIcalendarContent(content) val conferenceInfo = Factory.instance().createConferenceInfoFromIcalendarContent(content)
val conferenceUri = conferenceInfo?.uri?.asStringUriOnly() val conferenceUri = conferenceInfo?.uri?.asStringUriOnly()
if (conferenceInfo != null && conferenceUri != null) { if (conferenceInfo != null && conferenceUri != null) {
conferenceAddress.value = conferenceUri!! conferenceAddress = conferenceUri!!
Log.i( 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 conferenceSubject.value = conferenceInfo.subject
conferenceDescription.value = conferenceInfo.description conferenceDescription.value = conferenceInfo.description
@ -407,7 +419,7 @@ class ChatMessageContentData(
} }
fun callConferenceAddress() { fun callConferenceAddress() {
val address = conferenceAddress.value val address = conferenceAddress
if (address == null) { if (address == null) {
Log.e("[Content] Can't call null conference address!") Log.e("[Content] Can't call null conference address!")
return return

View file

@ -120,6 +120,17 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
} }
} }
private val keyboardVisibilityListener = object : AppUtils.KeyboardVisibilityListener {
override fun onKeyboardVisibilityChanged(visible: Boolean) {
if (visible && chatSendingViewModel.isEmojiPickerOpen.value == true) {
Log.d(
"[Chat Room] Emoji picker is opened, closing it because keyboard is now visible"
)
chatSendingViewModel.isEmojiPickerOpen.value = false
}
}
}
private lateinit var chatScrollListener: ChatScrollListener private lateinit var chatScrollListener: ChatScrollListener
override fun getLayoutId(): Int { override fun getLayoutId(): Int {
@ -202,15 +213,6 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
sharedViewModel.chatRoomFragmentOpenedEvent.value = Event(true) sharedViewModel.chatRoomFragmentOpenedEvent.value = Event(true)
} }
binding.root.addKeyboardInsetListener { keyboardVisible ->
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) Compatibility.setLocusIdInContentCaptureSession(binding.root, chatRoom)
isSecure = chatRoom.currentParams.isEncryptionEnabled isSecure = chatRoom.currentParams.isEncryptionEnabled
@ -469,7 +471,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
} else { } else {
if (path.isEmpty()) { if (path.isEmpty()) {
val name = content.name val name = content.name
if (name != null && name.isNotEmpty()) { if (!name.isNullOrEmpty()) {
val file = FileUtils.getFileStoragePath(name) val file = FileUtils.getFileStoragePath(name)
FileUtils.writeIntoFile(content.buffer, file) FileUtils.writeIntoFile(content.buffer, file)
path = file.absolutePath path = file.absolutePath
@ -478,8 +480,8 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
"[Chat Room] Content file path was empty, created file from buffer at $path" "[Chat Room] Content file path was empty, created file from buffer at $path"
) )
} else if (content.isIcalendar) { } else if (content.isIcalendar) {
val name = "conference.ics" val filename = "conference.ics"
val file = FileUtils.getFileStoragePath(name) val file = FileUtils.getFileStoragePath(filename)
FileUtils.writeIntoFile(content.buffer, file) FileUtils.writeIntoFile(content.buffer, file)
path = file.absolutePath path = file.absolutePath
content.filePath = path content.filePath = path
@ -498,20 +500,20 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
val extension = FileUtils.getExtensionFromFileName(path) val extension = FileUtils.getExtensionFromFileName(path)
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
when { when (FileUtils.getMimeType(mime)) {
FileUtils.isMimeImage(mime) -> navigateToImageFileViewer( FileUtils.MimeType.Image -> navigateToImageFileViewer(
preventScreenshots preventScreenshots
) )
FileUtils.isMimeVideo(mime) -> navigateToVideoFileViewer( FileUtils.MimeType.Video -> navigateToVideoFileViewer(
preventScreenshots preventScreenshots
) )
FileUtils.isMimeAudio(mime) -> navigateToAudioFileViewer( FileUtils.MimeType.Audio -> navigateToAudioFileViewer(
preventScreenshots preventScreenshots
) )
FileUtils.isMimePdf(mime) -> navigateToPdfFileViewer( FileUtils.MimeType.Pdf -> navigateToPdfFileViewer(
preventScreenshots preventScreenshots
) )
FileUtils.isMimePlainText(mime) -> navigateToTextFileViewer( FileUtils.MimeType.PlainText -> navigateToTextFileViewer(
preventScreenshots preventScreenshots
) )
else -> { else -> {
@ -584,9 +586,9 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
viewLifecycleOwner viewLifecycleOwner
) { ) {
it.consume { chatMessage -> it.consume { chatMessage ->
var index = 0 var index: Int
var retryCount = 0 var loadSteps = 0
var expectedChildCount = 0 var expectedChildCount: Int
do { do {
val events = listViewModel.events.value.orEmpty() val events = listViewModel.events.value.orEmpty()
expectedChildCount = events.size expectedChildCount = events.size
@ -600,18 +602,17 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
} }
index = events.indexOf(eventLog) index = events.indexOf(eventLog)
if (index == -1) { if (index == -1) {
retryCount += 1 loadSteps += 1
listViewModel.loadMoreData(events.size) listViewModel.loadMoreData(events.size)
} }
} while (index == -1 && retryCount < 5) } while (index == -1 && loadSteps < 5)
if (index != -1) { if (index != -1) {
if (retryCount == 0) { if (loadSteps == 0) {
scrollTo(index, true) scrollTo(index, true)
} else { } else {
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
val layoutManager = binding.chatMessagesList.layoutManager as LinearLayoutManager
var retryCount = 0 var retryCount = 0
do { do {
// We have to wait for newly loaded items to be added to list before being able to scroll // We have to wait for newly loaded items to be added to list before being able to scroll
@ -696,7 +697,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
} }
} }
binding.setVoiceRecordingTouchListener { view, event -> binding.setVoiceRecordingTouchListener { _, event ->
if (corePreferences.holdToRecordVoiceMessage) { if (corePreferences.holdToRecordVoiceMessage) {
when (event.action) { when (event.action) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
@ -846,7 +847,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
if (_adapter != null) { if (_adapter != null) {
try { try {
adapter.registerAdapterDataObserver(observer) adapter.registerAdapterDataObserver(observer)
} catch (ise: IllegalStateException) {} } catch (_: IllegalStateException) {}
} }
// Wait for items to be displayed // Wait for items to be displayed
@ -858,6 +859,10 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
"[Chat Room] Fragment resuming but viewModel lateinit property isn't initialized!" "[Chat Room] Fragment resuming but viewModel lateinit property isn't initialized!"
) )
} }
(requireActivity() as MainActivity).addKeyboardVisibilityListener(
keyboardVisibilityListener
)
} }
override fun onPause() { override fun onPause() {
@ -871,12 +876,16 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
if (_adapter != null) { if (_adapter != null) {
try { try {
adapter.unregisterAdapterDataObserver(observer) adapter.unregisterAdapterDataObserver(observer)
} catch (ise: IllegalStateException) {} } catch (_: IllegalStateException) {}
} }
// Conversation isn't visible anymore, any new message received in it will trigger a notification // Conversation isn't visible anymore, any new message received in it will trigger a notification
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
(requireActivity() as MainActivity).removeKeyboardVisibilityListener(
keyboardVisibilityListener
)
super.onPause() super.onPause()
} }

View file

@ -107,6 +107,8 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
val isEmojiPickerVisible = MutableLiveData<Boolean>() val isEmojiPickerVisible = MutableLiveData<Boolean>()
val isFileTransferAvailable = MutableLiveData<Boolean>()
val requestKeyboardHidingEvent: MutableLiveData<Event<Boolean>> by lazy { val requestKeyboardHidingEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>() MutableLiveData<Event<Boolean>>()
} }
@ -146,6 +148,7 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
sendMessageEnabled.value = false sendMessageEnabled.value = false
isEmojiPickerOpen.value = false isEmojiPickerOpen.value = false
isEmojiPickerVisible.value = corePreferences.showEmojiPickerButton isEmojiPickerVisible.value = corePreferences.showEmojiPickerButton
isFileTransferAvailable.value = LinphoneUtils.isFileTransferAvailable()
updateChatRoomReadOnlyState() updateChatRoomReadOnlyState()
} }
@ -174,6 +177,10 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
fun onTextToSendChanged(value: String) { fun onTextToSendChanged(value: String) {
sendMessageEnabled.value = value.trim().isNotEmpty() || attachments.value?.isNotEmpty() == true || isPendingVoiceRecord.value == true 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 (value.isNotEmpty()) {
if (attachFileEnabled.value == true && !corePreferences.allowMultipleFilesAndTextInSameMessage) { if (attachFileEnabled.value == true && !corePreferences.allowMultipleFilesAndTextInSameMessage) {
attachFileEnabled.value = false attachFileEnabled.value = false

View file

@ -25,6 +25,8 @@ import android.text.method.LinkMovementMethod
import android.util.AttributeSet import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.AppCompatTextView
import kotlin.math.ceil 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 * 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) { if (layout != null && layout.lineCount >= 2) {
val maxLineWidth = ceil(getMaxLineWidth(layout)).toInt() val maxLineWidth = ceil(getMaxLineWidth(layout)).toInt()
val uselessPaddingWidth = layout.width - maxLineWidth if (maxLineWidth < measuredWidth) {
val width = measuredWidth - uselessPaddingWidth super.onMeasure(
val height = measuredHeight MeasureSpec.makeMeasureSpec(maxLineWidth, MeasureSpec.getMode(widthSpec)),
setMeasuredDimension(width, height) heightSpec
)
}
} }
} }
@ -63,10 +67,8 @@ class MultiLineWrapContentWidthTextView : AppCompatTextView {
var maxWidth = 0.0f var maxWidth = 0.0f
val lines = layout.lineCount val lines = layout.lineCount
for (i in 0 until lines) { for (i in 0 until lines) {
if (layout.getLineWidth(i) > maxWidth) { maxWidth = max(maxWidth, layout.getLineWidth(i))
maxWidth = layout.getLineWidth(i)
} }
} return round(maxWidth)
return maxWidth
} }
} }

View file

@ -89,13 +89,26 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
address.value = conferenceAddress!! address.value = conferenceAddress!!
if (scheduleForLater.value == true && sendInviteViaChat.value == true) { if (scheduleForLater.value == true) {
if (sendInviteViaChat.value == true) {
// Send conference info even when conf is not scheduled for later // Send conference info even when conf is not scheduled for later
// as the conference server doesn't invite participants automatically // 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() val chatRoomParams = LinphoneUtils.getConferenceInvitationsChatRoomParams()
conferenceScheduler.sendInvitations(chatRoomParams) conferenceScheduler.sendInvitations(chatRoomParams)
} else { } else {
// Will be done in coreListener 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 {
Log.i("[Conference Creation] Group call is ready, leaving fragment")
conferenceCreationInProgress.value = false
conferenceCreationCompletedEvent.value = Event(true)
} }
} else if (state == ConferenceScheduler.State.Error) { } else if (state == ConferenceScheduler.State.Error) {
Log.e("[Conference Creation] Failed to create conference!") 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 { init {
sipContactsSelected.value = true sipContactsSelected.value = true
@ -191,11 +180,9 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
} }
conferenceScheduler.addListener(listener) conferenceScheduler.addListener(listener)
coreContext.core.addListener(coreListener)
} }
override fun onCleared() { override fun onCleared() {
coreContext.core.removeListener(coreListener)
conferenceScheduler.removeListener(listener) conferenceScheduler.removeListener(listener)
participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy) participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy)

View file

@ -398,13 +398,15 @@ class ConferenceWaitingRoomViewModel : MessageNotifierViewModel() {
private fun onAudioDevicesListUpdated() { private fun onAudioDevicesListUpdated() {
val bluetoothDeviceAvailable = AudioRouteUtils.isBluetoothAudioRouteAvailable() 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 audioRoutesEnabled.value = bluetoothDeviceAvailable
if (!bluetoothDeviceAvailable) { if (!bluetoothDeviceAvailable) {
audioRoutesSelected.value = false audioRoutesSelected.value = false
Log.w(
"[Conference Waiting Room] Bluetooth device no longer available, switching back to default microphone & earpiece/speaker"
)
if (isBluetoothHeadsetSelected.value == true) { if (isBluetoothHeadsetSelected.value == true) {
for (audioDevice in coreContext.core.audioDevices) { for (audioDevice in coreContext.core.audioDevices) {
if (isVideoEnabled.value == true) { if (isVideoEnabled.value == true) {

View file

@ -20,6 +20,7 @@
package org.linphone.activities.main.contact.fragments package org.linphone.activities.main.contact.fragments
import android.app.Activity import android.app.Activity
import android.app.Dialog
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Bundle 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.ContactEditorData
import org.linphone.activities.main.contact.data.NumberOrAddressEditorData import org.linphone.activities.main.contact.data.NumberOrAddressEditorData
import org.linphone.activities.main.contact.viewmodels.* import org.linphone.activities.main.contact.viewmodels.*
import org.linphone.activities.main.viewmodels.DialogViewModel
import org.linphone.activities.navigateToContact import org.linphone.activities.navigateToContact
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.databinding.ContactEditorFragmentBinding import org.linphone.databinding.ContactEditorFragmentBinding
import org.linphone.utils.DialogUtils
import org.linphone.utils.FileUtils import org.linphone.utils.FileUtils
import org.linphone.utils.PermissionHelper import org.linphone.utils.PermissionHelper
@ -71,10 +74,38 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
data.syncAccountName = null data.syncAccountName = null
data.syncAccountType = null data.syncAccountType = null
if (data.friend == null && corePreferences.showNewContactAccountDialog) { 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") Log.i("[Contact Editor] New contact, ask user where to store it")
SyncAccountPickerFragment(this).show(childFragmentManager, "SyncAccountPicker") SyncAccountPickerFragment(this).show(childFragmentManager, "SyncAccountPicker")
} else { } else {
Log.i("[Contact Editor] Saving new contact")
saveContact()
}
} else {
Log.i("[Contact Editor] Saving contact changes")
saveContact() saveContact()
} }
} }
@ -98,7 +129,7 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
} }
override fun onSyncAccountClicked(name: String?, type: String?) { 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.syncAccountName = name
data.syncAccountType = type data.syncAccountType = type
saveContact() saveContact()
@ -146,6 +177,9 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
Log.i("[Contact Editor] Displaying contact $savedContact") Log.i("[Contact Editor] Displaying contact $savedContact")
navigateToContact(id) navigateToContact(id)
} else { } else {
Log.w(
"[Contact Editor] Can't display $savedContact because it doesn't have a refKey, going back"
)
goBack() goBack()
} }
} }
@ -183,4 +217,35 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
startActivityForResult(chooserIntent, 0) 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()
}
} }

View file

@ -34,7 +34,7 @@ import org.linphone.activities.main.contact.data.ContactNumberOrAddressData
import org.linphone.activities.main.viewmodels.MessageNotifierViewModel import org.linphone.activities.main.viewmodels.MessageNotifierViewModel
import org.linphone.contact.ContactDataInterface import org.linphone.contact.ContactDataInterface
import org.linphone.contact.ContactsUpdatedListenerStub import org.linphone.contact.ContactsUpdatedListenerStub
import org.linphone.contact.hasPresence import org.linphone.contact.hasLongTermPresence
import org.linphone.core.* import org.linphone.core.*
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.utils.Event import org.linphone.utils.Event
@ -81,6 +81,8 @@ class ContactViewModel(friend: Friend, async: Boolean = false) : MessageNotifier
val readOnlyNativeAddressBook = MutableLiveData<Boolean>() val readOnlyNativeAddressBook = MutableLiveData<Boolean>()
val hasLongTermPresence = MutableLiveData<Boolean>()
private val chatRoomListener = object : ChatRoomListenerStub() { private val chatRoomListener = object : ChatRoomListenerStub() {
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) { override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
if (state == ChatRoom.State.Created) { if (state == ChatRoom.State.Created) {
@ -147,16 +149,19 @@ class ContactViewModel(friend: Friend, async: Boolean = false) : MessageNotifier
isNativeContact.postValue(friend.refKey != null) isNativeContact.postValue(friend.refKey != null)
presenceStatus.postValue(friend.consolidatedPresence) presenceStatus.postValue(friend.consolidatedPresence)
readOnlyNativeAddressBook.postValue(corePreferences.readOnlyNativeContacts) readOnlyNativeAddressBook.postValue(corePreferences.readOnlyNativeContacts)
hasLongTermPresence.postValue(friend.hasLongTermPresence())
} else { } else {
contact.value = friend contact.value = friend
displayName.value = friend.name displayName.value = friend.name
isNativeContact.value = friend.refKey != null isNativeContact.value = friend.refKey != null
presenceStatus.value = friend.consolidatedPresence presenceStatus.value = friend.consolidatedPresence
readOnlyNativeAddressBook.value = corePreferences.readOnlyNativeContacts readOnlyNativeAddressBook.value = corePreferences.readOnlyNativeContacts
hasLongTermPresence.value = friend.hasLongTermPresence()
} }
friend.addListener { friend.addListener {
presenceStatus.value = it.consolidatedPresence presenceStatus.value = it.consolidatedPresence
hasLongTermPresence.value = it.hasLongTermPresence()
} }
} }
@ -269,8 +274,4 @@ class ContactViewModel(friend: Friend, async: Boolean = false) : MessageNotifier
} }
numbersAndAddresses.postValue(list) numbersAndAddresses.postValue(list)
} }
fun hasPresence(): Boolean {
return contact.value?.hasPresence() ?: false
}
} }

View file

@ -119,17 +119,17 @@ class ContactsListViewModel : ViewModel() {
previousFilter = filterValue previousFilter = filterValue
val domain = if (sipContactsSelected.value == true) coreContext.core.defaultAccount?.params?.domain ?: "" else "" 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 val aggregation = MagicSearch.Aggregation.Friend
searchResultsPending = true searchResultsPending = true
fastFetchJob?.cancel() fastFetchJob?.cancel()
Log.i( 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( coreContext.contactsManager.magicSearch.getContactsListAsync(
filterValue, filterValue,
domain, domain,
filter, sources,
aggregation aggregation
) )

View file

@ -273,6 +273,24 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
// Don't check the following the previous permissions are being asked // Don't check the following the previous permissions are being asked
checkTelecomManagerPermissions() 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) @TargetApi(Version.API26_O_80)

View file

@ -134,7 +134,7 @@ class DialerViewModel : LogsUploadViewModel() {
) { ) {
if (result == VersionUpdateCheckResult.NewVersionAvailable) { if (result == VersionUpdateCheckResult.NewVersionAvailable) {
Log.i("[Dialer] Update available, version [$version], url [$url]") Log.i("[Dialer] Update available, version [$version], url [$url]")
if (url != null && url.isNotEmpty()) { if (!url.isNullOrEmpty()) {
updateAvailableEvent.value = Event(url) updateAvailableEvent.value = Event(url)
} }
} }

View file

@ -54,7 +54,11 @@ class PdfViewerFragment : GenericViewerFragment<FilePdfViewerFragmentBinding>()
)[PdfFileViewModel::class.java] )[PdfFileViewModel::class.java]
binding.viewModel = viewModel binding.viewModel = viewModel
viewModel.rendererReady.observe(viewLifecycleOwner) {
it.consume {
adapter = PdfPagesListAdapter(viewModel) adapter = PdfPagesListAdapter(viewModel)
binding.pdfViewPager.adapter = adapter binding.pdfViewPager.adapter = adapter
} }
}
}
} }

View file

@ -92,8 +92,8 @@ class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
val extension = FileUtils.getExtensionFromFileName(filePath) val extension = FileUtils.getExtensionFromFileName(filePath)
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
when { when (FileUtils.getMimeType(mime)) {
FileUtils.isMimeImage(mime) -> { FileUtils.MimeType.Image -> {
val export = lifecycleScope.async { val export = lifecycleScope.async {
Compatibility.addImageToMediaStore(requireContext(), content) Compatibility.addImageToMediaStore(requireContext(), content)
} }
@ -108,7 +108,7 @@ class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
) )
} }
} }
FileUtils.isMimeVideo(mime) -> { FileUtils.MimeType.Video -> {
val export = lifecycleScope.async { val export = lifecycleScope.async {
Compatibility.addVideoToMediaStore(requireContext(), content) Compatibility.addVideoToMediaStore(requireContext(), content)
} }
@ -123,7 +123,7 @@ class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
) )
} }
} }
FileUtils.isMimeAudio(mime) -> { FileUtils.MimeType.Audio -> {
val export = lifecycleScope.async { val export = lifecycleScope.async {
Compatibility.addAudioToMediaStore(requireContext(), content) Compatibility.addAudioToMediaStore(requireContext(), content)
} }

View file

@ -72,7 +72,7 @@ class AudioFileViewModel(content: Content) : FileViewerViewModel(content), Media
override fun getCurrentPosition(): Int { override fun getCurrentPosition(): Int {
try { try {
return mediaPlayer.currentPosition return mediaPlayer.currentPosition
} catch (ise: IllegalStateException) {} } catch (_: IllegalStateException) {}
return 0 return 0
} }

View file

@ -26,10 +26,15 @@ import android.widget.ImageView
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import java.io.File 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.LinphoneApplication.Companion.coreContext
import org.linphone.core.Content import org.linphone.core.Content
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.utils.Event
class PdfFileViewModelFactory(private val content: Content) : class PdfFileViewModelFactory(private val content: Content) :
ViewModelProvider.NewInstanceFactory() { ViewModelProvider.NewInstanceFactory() {
@ -43,31 +48,49 @@ class PdfFileViewModelFactory(private val content: Content) :
class PdfFileViewModel(content: Content) : FileViewerViewModel(content) { class PdfFileViewModel(content: Content) : FileViewerViewModel(content) {
val operationInProgress = MutableLiveData<Boolean>() val operationInProgress = MutableLiveData<Boolean>()
private val pdfRenderer: PdfRenderer val rendererReady = MutableLiveData<Event<Boolean>>()
private lateinit var pdfRenderer: PdfRenderer
init { init {
operationInProgress.value = false operationInProgress.value = false
val input = ParcelFileDescriptor.open(File(filePath), ParcelFileDescriptor.MODE_READ_ONLY) viewModelScope.launch {
withContext(Dispatchers.IO) {
val input = ParcelFileDescriptor.open(
File(filePath),
ParcelFileDescriptor.MODE_READ_ONLY
)
pdfRenderer = PdfRenderer(input) pdfRenderer = PdfRenderer(input)
Log.i("[PDF Viewer] ${pdfRenderer.pageCount} pages in file $filePath") Log.i("[PDF Viewer] ${pdfRenderer.pageCount} pages in file $filePath")
rendererReady.postValue(Event(true))
}
}
} }
override fun onCleared() { override fun onCleared() {
if (this::pdfRenderer.isInitialized) {
pdfRenderer.close() pdfRenderer.close()
}
super.onCleared() super.onCleared()
} }
fun getPagesCount(): Int { fun getPagesCount(): Int {
if (this::pdfRenderer.isInitialized) {
return pdfRenderer.pageCount return pdfRenderer.pageCount
} }
return 0
}
fun loadPdfPageInto(index: Int, view: ImageView) { fun loadPdfPageInto(index: Int, view: ImageView) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
try { try {
operationInProgress.value = true operationInProgress.postValue(true)
val page: PdfRenderer.Page = pdfRenderer.openPage(index) val page: PdfRenderer.Page = pdfRenderer.openPage(index)
val width = if (coreContext.screenWidth <= coreContext.screenHeight) coreContext.screenWidth else coreContext.screenHeight val width =
if (coreContext.screenWidth <= coreContext.screenHeight) coreContext.screenWidth else coreContext.screenHeight
val bm = Bitmap.createBitmap( val bm = Bitmap.createBitmap(
width.toInt(), width.toInt(),
(width / page.width * page.height).toInt(), (width / page.width * page.height).toInt(),
@ -76,12 +99,16 @@ class PdfFileViewModel(content: Content) : FileViewerViewModel(content) {
page.render(bm, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) page.render(bm, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
page.close() page.close()
withContext(Dispatchers.Main) {
view.setImageBitmap(bm) view.setImageBitmap(bm)
}
operationInProgress.value = false operationInProgress.postValue(false)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("[PDF Viewer] Exception: $e") Log.e("[PDF Viewer] Exception: $e")
operationInProgress.value = false operationInProgress.postValue(false)
}
}
} }
} }
} }

View file

@ -47,17 +47,10 @@ class TextFileViewModel(content: Content) : FileViewerViewModel(content) {
val text = MutableLiveData<String>() val text = MutableLiveData<String>()
init { init {
operationInProgress.value = false
openFile()
}
private fun openFile() {
operationInProgress.value = true
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
operationInProgress.postValue(true)
val br = BufferedReader(FileReader(filePath)) val br = BufferedReader(FileReader(filePath))
var line: String? var line: String?
val textBuilder = StringBuilder() val textBuilder = StringBuilder()

View file

@ -50,7 +50,7 @@ abstract class SecureFragment<T : ViewDataBinding> : GenericFragment<T>() {
override fun onResume() { override fun onResume() {
if (isSecure) { if (isSecure) {
enableSecureMode(isSecure) enableSecureMode(true)
} else { } else {
// This is a workaround to prevent a small blink showing the previous secured screen // This is a workaround to prevent a small blink showing the previous secured screen
lifecycleScope.launch { lifecycleScope.launch {

View file

@ -53,7 +53,9 @@ class DetailCallLogFragment : GenericFragment<HistoryDetailFragmentBinding>() {
viewModel = callLogGroup.lastCallLogViewModel viewModel = callLogGroup.lastCallLogViewModel
binding.viewModel = viewModel binding.viewModel = viewModel
if (viewModel.relatedCallLogs.value.orEmpty().isEmpty()) {
viewModel.addRelatedCallLogs(callLogGroup.callLogs) viewModel.addRelatedCallLogs(callLogGroup.callLogs)
}
useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false

View file

@ -44,7 +44,7 @@ class CallLogViewModel(val callLog: CallLog, private val isRelated: Boolean = fa
val statusIconResource: Int by lazy { val statusIconResource: Int by lazy {
if (callLog.dir == Call.Dir.Incoming) { if (callLog.dir == Call.Dir.Incoming) {
if (callLog.status == Call.Status.Missed) { if (LinphoneUtils.isCallLogMissed(callLog)) {
R.drawable.call_status_missed R.drawable.call_status_missed
} else { } else {
R.drawable.call_status_incoming R.drawable.call_status_incoming
@ -56,7 +56,7 @@ class CallLogViewModel(val callLog: CallLog, private val isRelated: Boolean = fa
val iconContentDescription: Int by lazy { val iconContentDescription: Int by lazy {
if (callLog.dir == Call.Dir.Incoming) { if (callLog.dir == Call.Dir.Incoming) {
if (callLog.status == Call.Status.Missed) { if (LinphoneUtils.isCallLogMissed(callLog)) {
R.string.content_description_missed_call R.string.content_description_missed_call
} else { } else {
R.string.content_description_incoming_call 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 { val directionIconResource: Int by lazy {
if (callLog.dir == Call.Dir.Incoming) { if (callLog.dir == Call.Dir.Incoming) {
if (callLog.status == Call.Status.Missed) { if (LinphoneUtils.isCallLogMissed(callLog)) {
R.drawable.call_missed R.drawable.call_missed
} else { } else {
R.drawable.call_incoming R.drawable.call_incoming

View file

@ -98,14 +98,6 @@ class AccountSettingsFragment : GenericSettingFragment<SettingsAccountFragmentBi
} }
} }
viewModel.publishPresenceToggledEvent.observe(
viewLifecycleOwner
) {
it.consume {
sharedViewModel.publishPresenceToggled.value = true
}
}
viewModel.deleteAccountRequiredEvent.observe( viewModel.deleteAccountRequiredEvent.observe(
viewLifecycleOwner viewLifecycleOwner
) { ) {

View file

@ -74,6 +74,14 @@ class ContactsSettingsFragment : GenericSettingFragment<SettingsContactsFragment
} }
} }
viewModel.publishPresenceToggledEvent.observe(
viewLifecycleOwner
) {
it.consume {
sharedViewModel.publishPresenceToggled.value = true
}
}
viewModel.ldapNewSettingsListener = object : SettingListenerStub() { viewModel.ldapNewSettingsListener = object : SettingListenerStub() {
override fun onClicked() { override fun onClicked() {
Log.i("[Contacts Settings] Clicked on new LDAP config") Log.i("[Contacts Settings] Clicked on new LDAP config")

View file

@ -75,10 +75,6 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
MutableLiveData<Event<Boolean>>() MutableLiveData<Event<Boolean>>()
} }
val publishPresenceToggledEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val displayUsernameInsteadOfIdentity = corePreferences.replaceSipUriByUsername val displayUsernameInsteadOfIdentity = corePreferences.replaceSipUriByUsername
private var accountToDelete: Account? = null private var accountToDelete: Account? = null
@ -293,7 +289,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
account.params = params account.params = params
transportIndex.value = account.params.transport.toInt() transportIndex.value = account.params.transport.toInt()
} else { } 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<String>() val limeServerUrl = MutableLiveData<String>()
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<Boolean>()
val disableBundleModeListener = object : SettingListenerStub() { val disableBundleModeListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) { override fun onBoolValueChanged(newValue: Boolean) {
val params = account.params.clone() val params = account.params.clone()
@ -518,7 +504,6 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
limeServerUrl.value = params.limeServerUrl limeServerUrl.value = params.limeServerUrl
hideLinkPhoneNumber.value = corePreferences.hideLinkPhoneNumber || params.identityAddress?.domain != corePreferences.defaultDomain hideLinkPhoneNumber.value = corePreferences.hideLinkPhoneNumber || params.identityAddress?.domain != corePreferences.defaultDomain
publishPresence.value = params.isPublishEnabled
disableBundleMode.value = !params.isRtpBundleEnabled disableBundleMode.value = !params.isRtpBundleEnabled
} }

View file

@ -141,7 +141,7 @@ class AudioSettingsViewModel : GenericSettingsViewModel() {
override fun onTextValueChanged(newValue: String) { override fun onTextValueChanged(newValue: String) {
try { try {
core.micGainDb = newValue.toFloat() core.micGainDb = newValue.toFloat()
} catch (nfe: NumberFormatException) { } catch (_: NumberFormatException) {
} }
} }
} }
@ -151,7 +151,7 @@ class AudioSettingsViewModel : GenericSettingsViewModel() {
override fun onTextValueChanged(newValue: String) { override fun onTextValueChanged(newValue: String) {
try { try {
core.playbackGainDb = newValue.toFloat() core.playbackGainDb = newValue.toFloat()
} catch (nfe: NumberFormatException) { } catch (_: NumberFormatException) {
} }
} }
} }

View file

@ -97,7 +97,7 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
Log.w("[Call Settings] Disabling Telecom Manager auto-enable") Log.w("[Call Settings] Disabling Telecom Manager auto-enable")
prefs.manuallyDisabledTelecomManager = true prefs.manuallyDisabledTelecomManager = true
} }
prefs.useTelecomManager = newValue prefs.useTelecomManager = false
} }
} }
} }
@ -169,7 +169,7 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
override fun onTextValueChanged(newValue: String) { override fun onTextValueChanged(newValue: String) {
try { try {
prefs.autoAnswerDelay = newValue.toInt() prefs.autoAnswerDelay = newValue.toInt()
} catch (nfe: NumberFormatException) { } catch (_: NumberFormatException) {
} }
} }
} }
@ -179,7 +179,7 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
override fun onTextValueChanged(newValue: String) { override fun onTextValueChanged(newValue: String) {
try { try {
core.incTimeout = newValue.toInt() core.incTimeout = newValue.toInt()
} catch (nfe: NumberFormatException) { } catch (_: NumberFormatException) {
} }
} }
} }

View file

@ -61,7 +61,7 @@ class ChatSettingsViewModel : GenericSettingsViewModel() {
val maxSize = newValue.toInt() val maxSize = newValue.toInt()
core.maxSizeForAutoDownloadIncomingFiles = maxSize core.maxSizeForAutoDownloadIncomingFiles = maxSize
updateAutoDownloadIndexFromMaxSize(maxSize) updateAutoDownloadIndexFromMaxSize(maxSize)
} catch (nfe: NumberFormatException) { } catch (_: NumberFormatException) {
} }
} }
} }

View file

@ -22,6 +22,8 @@ package org.linphone.activities.main.settings.viewmodels
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.activities.main.settings.SettingListenerStub 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.Event
import org.linphone.utils.PermissionHelper import org.linphone.utils.PermissionHelper
@ -40,6 +42,30 @@ class ContactsSettingsViewModel : GenericSettingsViewModel() {
val friendListSubscribe = MutableLiveData<Boolean>() val friendListSubscribe = MutableLiveData<Boolean>()
val rlsAddressAvailable = MutableLiveData<Boolean>() val rlsAddressAvailable = MutableLiveData<Boolean>()
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<Boolean>()
val publishPresenceToggledEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val showNewContactAccountDialogListener = object : SettingListenerStub() { val showNewContactAccountDialogListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) { override fun onBoolValueChanged(newValue: Boolean) {
prefs.showNewContactAccountDialog = newValue prefs.showNewContactAccountDialog = newValue
@ -51,12 +77,12 @@ class ContactsSettingsViewModel : GenericSettingsViewModel() {
override fun onBoolValueChanged(newValue: Boolean) { override fun onBoolValueChanged(newValue: Boolean) {
if (newValue) { if (newValue) {
if (PermissionHelper.get().hasWriteContactsPermission()) { if (PermissionHelper.get().hasWriteContactsPermission()) {
prefs.storePresenceInNativeContact = newValue prefs.storePresenceInNativeContact = true
} else { } else {
askWriteContactsPermissionForPresenceStorageEvent.value = Event(true) askWriteContactsPermissionForPresenceStorageEvent.value = Event(true)
} }
} else { } else {
prefs.storePresenceInNativeContact = newValue prefs.storePresenceInNativeContact = false
} }
} }
} }
@ -97,6 +123,8 @@ class ContactsSettingsViewModel : GenericSettingsViewModel() {
friendListSubscribe.value = core.isFriendListSubscriptionEnabled friendListSubscribe.value = core.isFriendListSubscriptionEnabled
rlsAddressAvailable.value = !core.config.getString("sip", "rls_uri", "").isNullOrEmpty() rlsAddressAvailable.value = !core.config.getString("sip", "rls_uri", "").isNullOrEmpty()
publishPresence.value = prefs.publishPresence
showNewContactAccountDialog.value = prefs.showNewContactAccountDialog showNewContactAccountDialog.value = prefs.showNewContactAccountDialog
nativePresence.value = prefs.storePresenceInNativeContact nativePresence.value = prefs.storePresenceInNativeContact
showOrganization.value = prefs.displayOrganization showOrganization.value = prefs.displayOrganization

View file

@ -52,7 +52,7 @@ class NetworkSettingsViewModel : GenericSettingsViewModel() {
try { try {
val port = newValue.toInt() val port = newValue.toInt()
setTransportPort(port) setTransportPort(port)
} catch (nfe: NumberFormatException) { } catch (_: NumberFormatException) {
} }
} }
} }

View file

@ -43,7 +43,7 @@ class TunnelSettingsViewModel : GenericSettingsViewModel() {
val config = getTunnelConfig() val config = getTunnelConfig()
config.port = newValue.toInt() config.port = newValue.toInt()
updateTunnelConfig(config) updateTunnelConfig(config)
} catch (nfe: NumberFormatException) { } catch (_: NumberFormatException) {
} }
} }
} }
@ -72,7 +72,7 @@ class TunnelSettingsViewModel : GenericSettingsViewModel() {
val config = getTunnelConfig() val config = getTunnelConfig()
config.port2 = newValue.toInt() config.port2 = newValue.toInt()
updateTunnelConfig(config) updateTunnelConfig(config)
} catch (nfe: NumberFormatException) { } catch (_: NumberFormatException) {
} }
} }
} }

View file

@ -87,7 +87,17 @@ class VideoSettingsViewModel : GenericSettingsViewModel() {
val videoPresetListener = object : SettingListenerStub() { val videoPresetListener = object : SettingListenerStub() {
override fun onListValueChanged(position: Int) { override fun onListValueChanged(position: Int) {
videoPresetIndex.value = position // Needed to display/hide two below settings 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<Int>() val videoPresetIndex = MutableLiveData<Int>()
@ -106,7 +116,7 @@ class VideoSettingsViewModel : GenericSettingsViewModel() {
try { try {
core.downloadBandwidth = newValue.toInt() core.downloadBandwidth = newValue.toInt()
core.uploadBandwidth = newValue.toInt() core.uploadBandwidth = newValue.toInt()
} catch (nfe: NumberFormatException) { } catch (_: NumberFormatException) {
} }
} }
} }

View file

@ -63,7 +63,7 @@ class ListTopBarViewModel : ViewModel() {
val list = arrayListOf<Int>() val list = arrayListOf<Int>()
selectedItems.value = list selectedItems.value = list
isSelectionNotEmpty.value = list.isNotEmpty() isSelectionNotEmpty.value = false
} }
fun onToggleSelect(position: Int) { fun onToggleSelect(position: Int) {

View file

@ -62,8 +62,6 @@ class CallsListFragment : GenericVideoPreviewFragment<VoipCallsListFragmentBindi
binding.controlsViewModel = controlsViewModel binding.controlsViewModel = controlsViewModel
setupLocalViewPreview(binding.localPreviewVideoSurface, binding.switchCamera)
binding.setCancelClickListener { binding.setCancelClickListener {
goBack() goBack()
} }
@ -96,6 +94,18 @@ class CallsListFragment : GenericVideoPreviewFragment<VoipCallsListFragmentBindi
} }
} }
override fun onResume() {
super.onResume()
setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera)
}
override fun onPause() {
super.onPause()
cleanUpLocalVideoPreview(binding.localPreviewVideoSurface)
}
private fun showCallMenu(anchor: View, callData: CallData) { private fun showCallMenu(anchor: View, callData: CallData) {
val popupView: VoipCallContextMenuBindingImpl = DataBindingUtil.inflate( val popupView: VoipCallContextMenuBindingImpl = DataBindingUtil.inflate(
LayoutInflater.from(requireContext()), LayoutInflater.from(requireContext()),

View file

@ -23,7 +23,6 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.navigation.navGraphViewModels import androidx.navigation.navGraphViewModels
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R import org.linphone.R
import org.linphone.activities.voip.ConferenceDisplayMode import org.linphone.activities.voip.ConferenceDisplayMode
import org.linphone.activities.voip.viewmodels.ConferenceViewModel import org.linphone.activities.voip.viewmodels.ConferenceViewModel
@ -45,8 +44,6 @@ class ConferenceLayoutFragment : GenericVideoPreviewFragment<VoipConferenceLayou
binding.controlsViewModel = controlsViewModel binding.controlsViewModel = controlsViewModel
setupLocalViewPreview(binding.localPreviewVideoSurface, binding.switchCamera)
binding.setCancelClickListener { binding.setCancelClickListener {
goBack() goBack()
} }
@ -84,7 +81,13 @@ class ConferenceLayoutFragment : GenericVideoPreviewFragment<VoipConferenceLayou
showTooManyParticipantsForMosaicLayoutDialog() showTooManyParticipantsForMosaicLayoutDialog()
} }
coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera)
}
override fun onPause() {
super.onPause()
cleanUpLocalVideoPreview(binding.localPreviewVideoSurface)
} }
private fun showTooManyParticipantsForMosaicLayoutDialog() { private fun showTooManyParticipantsForMosaicLayoutDialog() {

View file

@ -23,7 +23,6 @@ import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.navigation.navGraphViewModels import androidx.navigation.navGraphViewModels
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R import org.linphone.R
import org.linphone.activities.navigateToAddParticipants import org.linphone.activities.navigateToAddParticipants
import org.linphone.activities.voip.viewmodels.ConferenceViewModel import org.linphone.activities.voip.viewmodels.ConferenceViewModel
@ -49,8 +48,6 @@ class ConferenceParticipantsFragment : GenericVideoPreviewFragment<VoipConferenc
binding.controlsViewModel = controlsViewModel binding.controlsViewModel = controlsViewModel
setupLocalViewPreview(binding.localPreviewVideoSurface, binding.switchCamera)
conferenceViewModel.conferenceExists.observe( conferenceViewModel.conferenceExists.observe(
viewLifecycleOwner viewLifecycleOwner
) { exists -> ) { exists ->
@ -90,12 +87,13 @@ class ConferenceParticipantsFragment : GenericVideoPreviewFragment<VoipConferenc
super.onResume() super.onResume()
skipEvents = false skipEvents = false
coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
skipEvents = true skipEvents = true
cleanUpLocalVideoPreview(binding.localPreviewVideoSurface)
} }
} }

View file

@ -34,7 +34,6 @@ abstract class GenericVideoPreviewFragment<T : ViewDataBinding> : GenericFragmen
private var switchY: Float = 0f private var switchY: Float = 0f
private var switchCameraImageView: ImageView? = null private var switchCameraImageView: ImageView? = null
private lateinit var videoPreviewTextureView: TextureView
private val previewTouchListener = View.OnTouchListener { view, event -> private val previewTouchListener = View.OnTouchListener { view, event ->
when (event.action) { when (event.action) {
@ -67,19 +66,13 @@ abstract class GenericVideoPreviewFragment<T : ViewDataBinding> : GenericFragmen
} }
} }
protected fun setupLocalViewPreview(localVideoPreview: TextureView, switchCamera: ImageView?) { protected fun setupLocalVideoPreview(localVideoPreview: TextureView, switchCamera: ImageView?) {
if (coreContext.core.currentCall?.currentParams?.isVideoEnabled == true) {
videoPreviewTextureView = localVideoPreview
switchCameraImageView = switchCamera switchCameraImageView = switchCamera
videoPreviewTextureView.setOnTouchListener(previewTouchListener) localVideoPreview.setOnTouchListener(previewTouchListener)
} coreContext.core.nativePreviewWindowId = localVideoPreview
} }
override fun onResume() { protected fun cleanUpLocalVideoPreview(localVideoPreview: TextureView) {
super.onResume() localVideoPreview.setOnTouchListener(null)
if (::videoPreviewTextureView.isInitialized) {
coreContext.core.nativePreviewWindowId = videoPreviewTextureView
}
} }
} }

View file

@ -24,7 +24,6 @@ import android.os.SystemClock
import android.view.View import android.view.View
import android.widget.Chronometer import android.widget.Chronometer
import androidx.navigation.navGraphViewModels import androidx.navigation.navGraphViewModels
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R import org.linphone.R
import org.linphone.activities.navigateToActiveCall import org.linphone.activities.navigateToActiveCall
import org.linphone.activities.voip.viewmodels.CallsViewModel import org.linphone.activities.voip.viewmodels.CallsViewModel
@ -46,8 +45,6 @@ class OutgoingCallFragment : GenericVideoPreviewFragment<VoipCallOutgoingFragmen
binding.callsViewModel = callsViewModel binding.callsViewModel = callsViewModel
setupLocalViewPreview(binding.localPreviewVideoSurface, binding.switchCamera)
callsViewModel.callConnectedEvent.observe( callsViewModel.callConnectedEvent.observe(
viewLifecycleOwner viewLifecycleOwner
) { ) {
@ -79,7 +76,7 @@ class OutgoingCallFragment : GenericVideoPreviewFragment<VoipCallOutgoingFragmen
viewLifecycleOwner viewLifecycleOwner
) { ) {
if (it) { if (it) {
coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera)
} }
} }
} }
@ -87,11 +84,17 @@ class OutgoingCallFragment : GenericVideoPreviewFragment<VoipCallOutgoingFragmen
// We don't want the proximity sensor to turn screen OFF in this fragment // We don't want the proximity sensor to turn screen OFF in this fragment
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
controlsViewModel.forceDisableProximitySensor.value = true controlsViewModel.forceDisableProximitySensor.value = true
if (controlsViewModel.isOutgoingEarlyMedia.value == true) {
setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera)
}
} }
override fun onPause() { override fun onPause() {
controlsViewModel.forceDisableProximitySensor.value = false
super.onPause() super.onPause()
controlsViewModel.forceDisableProximitySensor.value = false
cleanUpLocalVideoPreview(binding.localPreviewVideoSurface)
} }
} }

View file

@ -67,8 +67,6 @@ class SingleCallFragment : GenericVideoPreviewFragment<VoipSingleCallFragmentBin
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
setupLocalViewPreview(binding.localPreviewVideoSurface, binding.switchCamera)
binding.controlsViewModel = controlsViewModel binding.controlsViewModel = controlsViewModel
binding.callsViewModel = callsViewModel binding.callsViewModel = callsViewModel
@ -89,7 +87,7 @@ class SingleCallFragment : GenericVideoPreviewFragment<VoipSingleCallFragmentBin
) )
navigateToIncomingCall() navigateToIncomingCall()
} }
Call.State.OutgoingInit, Call.State.OutgoingProgress, Call.State.OutgoingRinging, Call.State.OutgoingEarlyMedia -> { Call.State.OutgoingRinging, Call.State.OutgoingEarlyMedia -> {
Log.i( Log.i(
"[Single Call] New current call is in [$callState] state, switching to OutgoingCall fragment" "[Single Call] New current call is in [$callState] state, switching to OutgoingCall fragment"
) )
@ -193,15 +191,20 @@ class SingleCallFragment : GenericVideoPreviewFragment<VoipSingleCallFragmentBin
startActivity(intent) startActivity(intent)
} }
} }
}
override fun onResume() {
super.onResume()
coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface
coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
controlsViewModel.hideExtraButtons(true) controlsViewModel.hideExtraButtons(true)
cleanUpLocalVideoPreview(binding.localPreviewVideoSurface)
} }
private fun showCallVideoUpdateDialog(call: Call) { private fun showCallVideoUpdateDialog(call: Call) {

View file

@ -84,8 +84,11 @@ class StatusFragment : GenericFragment<VoipStatusFragmentBinding>() {
private fun showZrtpDialog(call: Call) { private fun showZrtpDialog(call: Call) {
if (zrtpDialog != null && zrtpDialog?.isShowing == true) { if (zrtpDialog != null && zrtpDialog?.isShowing == true) {
Log.e("[Status Fragment] ZRTP dialog already visible") Log.w(
return "[Status Fragment] ZRTP dialog already visible, closing it and creating a new one"
)
zrtpDialog?.dismiss()
zrtpDialog = null
} }
val token = call.authenticationToken val token = call.authenticationToken
@ -125,8 +128,19 @@ class StatusFragment : GenericFragment<VoipStatusFragmentBinding>() {
viewModel.showCancelButton( viewModel.showCancelButton(
{ {
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 call.authenticationTokenVerified = false
this@StatusFragment.viewModel.updateEncryptionInfo(call) }
} else {
Log.e(
"[Status Fragment] Can't decline the ZRTP SAS token, call is in state [${call.state}]"
)
}
dialog.dismiss() dialog.dismiss()
zrtpDialog = null zrtpDialog = null
}, },
@ -135,8 +149,13 @@ class StatusFragment : GenericFragment<VoipStatusFragmentBinding>() {
viewModel.showOkButton( viewModel.showOkButton(
{ {
if (call.state != Call.State.End && call.state != Call.State.Released) {
call.authenticationTokenVerified = true call.authenticationTokenVerified = true
this@StatusFragment.viewModel.updateEncryptionInfo(call) } else {
Log.e(
"[Status Fragment] Can't verify the ZRTP SAS token, call is in state [${call.state}]"
)
}
dialog.dismiss() dialog.dismiss()
zrtpDialog = null zrtpDialog = null
}, },

View file

@ -81,6 +81,8 @@ class ControlsViewModel : ViewModel() {
val attendedTransfer = MutableLiveData<Boolean>() val attendedTransfer = MutableLiveData<Boolean>()
val chatDisabled = MutableLiveData<Boolean>()
val goToConferenceParticipantsListEvent: MutableLiveData<Event<Boolean>> by lazy { val goToConferenceParticipantsListEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>() MutableLiveData<Event<Boolean>>()
} }
@ -206,6 +208,7 @@ class ControlsViewModel : ViewModel() {
init { init {
coreContext.core.addListener(listener) coreContext.core.addListener(listener)
chatDisabled.value = corePreferences.disableChat
fullScreenMode.value = false fullScreenMode.value = false
extraButtonsMenuTranslateY.value = AppUtils.getDimension( extraButtonsMenuTranslateY.value = AppUtils.getDimension(
R.dimen.voip_call_extra_buttons_translate_y R.dimen.voip_call_extra_buttons_translate_y
@ -536,7 +539,7 @@ class ControlsViewModel : ViewModel() {
isVideoEnabled.value = enabled isVideoEnabled.value = enabled
showTakeSnapshotButton.value = enabled && corePreferences.showScreenshotButton 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 val videoDirection = coreContext.core.currentCall?.currentParams?.videoDirection
videoDirection == MediaDirection.SendRecv || videoDirection == MediaDirection.SendOnly videoDirection == MediaDirection.SendRecv || videoDirection == MediaDirection.SendOnly
} else { } else {

View file

@ -42,6 +42,8 @@ class StatusViewModel : StatusViewModel() {
MutableLiveData<Event<Boolean>>() MutableLiveData<Event<Boolean>>()
} }
var previouslyDeclineToken = false
private val listener = object : CoreListenerStub() { private val listener = object : CoreListenerStub() {
override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) { override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) {
updateCallQualityIcon() updateCallQualityIcon()
@ -54,8 +56,11 @@ class StatusViewModel : StatusViewModel() {
authenticationToken: String? authenticationToken: String?
) { ) {
updateEncryptionInfo(call) updateEncryptionInfo(call)
if (call.currentParams.mediaEncryption == MediaEncryption.ZRTP && !call.authenticationTokenVerified && call.authenticationToken != null) { // Check if we just declined a previously validated token
showZrtpDialogEvent.value = Event(call) // 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 val currentCall = coreContext.core.currentCall
if (currentCall != null) { if (currentCall != null) {
updateEncryptionInfo(currentCall) updateEncryptionInfo(currentCall)
showZrtpDialog(currentCall)
if (currentCall.currentParams.mediaEncryption == MediaEncryption.ZRTP && !currentCall.authenticationTokenVerified && currentCall.authenticationToken != null) {
showZrtpDialogEvent.value = Event(currentCall)
}
} }
} }
@ -94,8 +96,8 @@ class StatusViewModel : StatusViewModel() {
fun showZrtpDialog() { fun showZrtpDialog() {
val currentCall = coreContext.core.currentCall val currentCall = coreContext.core.currentCall
if (currentCall?.authenticationToken != null && currentCall.currentParams.mediaEncryption == MediaEncryption.ZRTP) { if (currentCall != null) {
showZrtpDialogEvent.value = Event(currentCall) showZrtpDialog(currentCall, force = true)
} }
} }
@ -112,6 +114,9 @@ class StatusViewModel : StatusViewModel() {
encryptionContentDescription.value = R.string.content_description_call_secured encryptionContentDescription.value = R.string.content_description_call_secured
return return
} }
if (call.state == Call.State.End || call.state == Call.State.Released) {
return
}
when (call.currentParams.mediaEncryption ?: MediaEncryption.None) { when (call.currentParams.mediaEncryption ?: MediaEncryption.None) {
MediaEncryption.SRTP, MediaEncryption.DTLS -> { 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() { private fun updateCallQualityIcon() {
val call = coreContext.core.currentCall ?: coreContext.core.calls.firstOrNull() val call = coreContext.core.currentCall ?: coreContext.core.calls.firstOrNull()
val quality = call?.currentQuality ?: 0f val quality = call?.currentQuality ?: 0f

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.compatibility
import android.annotation.TargetApi
import android.content.ClipboardManager
@TargetApi(28)
class Api28Compatibility {
companion object {
fun clearClipboard(clipboard: ClipboardManager) {
clipboard.clearPrimaryClip()
}
}
}

View file

@ -262,6 +262,10 @@ class Api31Compatibility {
context.startForegroundService(intent) context.startForegroundService(intent)
} catch (fssnae: ForegroundServiceStartNotAllowedException) { } catch (fssnae: ForegroundServiceStartNotAllowedException) {
Log.e("[Api31 Compatibility] Can't start service as foreground! $fssnae") 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) service.startForeground(notifId, notif)
} catch (fssnae: ForegroundServiceStartNotAllowedException) { } catch (fssnae: ForegroundServiceStartNotAllowedException) {
Log.e("[Api31 Compatibility] Can't start service as foreground! $fssnae") 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")
} }
} }

View file

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

View file

@ -23,6 +23,7 @@ import android.app.Activity
import android.app.Notification import android.app.Notification
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap 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)) { if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
Api31Compatibility.startForegroundService(service, notifId, notif) Api31Compatibility.startForegroundService(service, notifId, notif)
} else { } 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 */ /* Call */
fun canDrawOverlay(context: Context): Boolean { fun canDrawOverlay(context: Context): Boolean {
@ -455,5 +472,26 @@ class Compatibility {
} }
return false 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
}
} }
} }

View file

@ -88,7 +88,7 @@ abstract class GenericContactViewModel(private val sipAddress: Address) : Messag
contactLookup() contactLookup()
} }
protected fun contactLookup() { private fun contactLookup() {
displayName.value = LinphoneUtils.getDisplayName(sipAddress) displayName.value = LinphoneUtils.getDisplayName(sipAddress)
val friend = coreContext.contactsManager.findContactByAddress(sipAddress) val friend = coreContext.contactsManager.findContactByAddress(sipAddress)
if (friend != null) { if (friend != null) {

View file

@ -35,7 +35,6 @@ import androidx.core.app.Person
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import java.io.IOException import java.io.IOException
import kotlinx.coroutines.*
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
@ -388,7 +387,7 @@ fun Friend.getContactForPhoneNumberOrAddress(value: String): String? {
return null return null
} }
fun Friend.hasPresence(): Boolean { fun Friend.hasLongTermPresence(): Boolean {
for (address in addresses) { for (address in addresses) {
val presenceModel = getPresenceModelForUriOrTel(address.asStringUriOnly()) val presenceModel = getPresenceModelForUriOrTel(address.asStringUriOnly())
if (presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open) return true 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 // Check that the URI points to a real file
val contentResolver = coreContext.context.contentResolver val contentResolver = coreContext.context.contentResolver
try { try {
if (contentResolver.openAssetFileDescriptor(pictureUri, "r") != null) { val fd = contentResolver.openAssetFileDescriptor(pictureUri, "r")
if (fd != null) {
fd.close()
return pictureUri return pictureUri
} }
} catch (ioe: IOException) { } } catch (_: IOException) { }
} }
// Fallback to thumbnail if high res picture isn't available // Fallback to thumbnail if high res picture isn't available
@ -432,11 +433,11 @@ fun Friend.getPictureUri(thumbnailPreferred: Boolean = false): Uri? {
lookupUri, lookupUri,
ContactsContract.Contacts.Photo.CONTENT_DIRECTORY ContactsContract.Contacts.Photo.CONTENT_DIRECTORY
) )
} catch (e: Exception) { } } catch (_: Exception) { }
} else if (photo != null) { } else if (photo != null) {
try { try {
return Uri.parse(photo) return Uri.parse(photo)
} catch (e: Exception) { } } catch (_: Exception) { }
} }
return null return null
} }

View file

@ -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?) { override fun onPushNotificationReceived(core: Core, payload: String?) {
Log.i("[Context] Push notification received: $payload") Log.i("[Context] Push notification received: $payload")
} }
@ -431,18 +425,22 @@ class CoreContext(
} }
fun onForeground() { fun onForeground() {
// If presence publish is disabled and we call core.setConsolidatedPresence, it will enabled it! // We can't rely on defaultAccount?.params?.isPublishEnabled
if (core.defaultAccount?.params?.isPublishEnabled == true) { // as it will be modified by the SDK when changing the presence status
Log.i("[Context] App is in foreground, setting consolidated presence to Online") if (corePreferences.publishPresence) {
Log.i("[Context] App is in foreground, PUBLISHING presence as Online")
core.consolidatedPresence = ConsolidatedPresence.Online core.consolidatedPresence = ConsolidatedPresence.Online
} }
} }
fun onBackground() { fun onBackground() {
// If presence publish is disabled and we call core.setConsolidatedPresence, it will enabled it! // We can't rely on defaultAccount?.params?.isPublishEnabled
if (core.defaultAccount?.params?.isPublishEnabled == true) { // as it will be modified by the SDK when changing the presence status
Log.i("[Context] App is in background, setting consolidated presence to Busy") if (corePreferences.publishPresence) {
core.consolidatedPresence = ConsolidatedPresence.Busy 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 extension = FileUtils.getExtensionFromFileName(filePath)
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
when { when (FileUtils.getMimeType(mime)) {
FileUtils.isMimeImage(mime) -> { FileUtils.MimeType.Image -> {
if (Compatibility.addImageToMediaStore(context, content)) { if (Compatibility.addImageToMediaStore(context, content)) {
Log.i( Log.i(
"[Context] Successfully exported image [${content.name}] to Media Store" "[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)) { if (Compatibility.addVideoToMediaStore(context, content)) {
Log.i( Log.i(
"[Context] Successfully exported video [${content.name}] to Media Store" "[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)) { if (Compatibility.addAudioToMediaStore(context, content)) {
Log.i( Log.i(
"[Context] Successfully exported audio [${content.name}] to Media Store" "[Context] Successfully exported audio [${content.name}] to Media Store"

View file

@ -53,6 +53,7 @@ class CorePreferences constructor(private val context: Context) {
context, context,
MasterKey.DEFAULT_MASTER_KEY_ALIAS MasterKey.DEFAULT_MASTER_KEY_ALIAS
).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build() ).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
try { try {
EncryptedSharedPreferences.create( EncryptedSharedPreferences.create(
context, context,
@ -280,6 +281,12 @@ class CorePreferences constructor(private val context: Context) {
config.setBool("app", "contact_shortcuts", value) 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 */ /* Call */
var sendEarlyMedia: Boolean var sendEarlyMedia: Boolean

View file

@ -34,27 +34,33 @@ class CoreService : CoreService() {
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 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) { if (corePreferences.keepServiceAlive) {
Log.i("[Service] Starting as foreground to keep app alive in background") Log.i("[Service] Starting as foreground to keep app alive in background")
if (!ensureCoreExists( val contextCreated = ensureCoreExists(
applicationContext, applicationContext,
pushReceived = false, pushReceived = false,
service = this, service = this,
useAutoStartDescription = false useAutoStartDescription = false
) )
) { if (!contextCreated) {
// Only start foreground notification if context already exists, otherwise context will do it itself
coreContext.notificationsManager.startForeground(this, false) coreContext.notificationsManager.startForeground(this, false)
} }
} else if (intent?.extras?.get("StartForeground") == true) { } else if (intent?.extras?.get("StartForeground") == true) {
Log.i("[Service] Starting as foreground due to device boot or app update") Log.i("[Service] Starting as foreground due to device boot or app update")
if (!ensureCoreExists( val contextCreated = ensureCoreExists(
applicationContext, applicationContext,
pushReceived = false, pushReceived = false,
service = this, service = this,
useAutoStartDescription = true 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.notificationsManager.startForeground(this, true)
} }
coreContext.checkIfForegroundServiceNotificationCanBeRemovedAfterDelay(5000) coreContext.checkIfForegroundServiceNotificationCanBeRemovedAfterDelay(5000)

View file

@ -84,7 +84,7 @@ class NotificationsManager(private val context: Context) {
const val INTENT_REMOTE_ADDRESS = "REMOTE_ADDRESS" const val INTENT_REMOTE_ADDRESS = "REMOTE_ADDRESS"
private const val SERVICE_NOTIF_ID = 1 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" const val CHAT_TAG = "Chat"
private const val MISSED_CALL_TAG = "Missed call" 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()) { if (notifiable.messages.isNotEmpty()) {
displayChatNotifiable(room, notifiable) displayChatNotifiable(room, notifiable)
} else { } else {
@ -333,7 +349,7 @@ class NotificationsManager(private val context: Context) {
Log.w( Log.w(
"[Notifications Manager] Found existing call? notification [${notification.id}], cancelling it" "[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) { } else if (notification.tag == CHAT_TAG) {
Log.i( Log.i(
"[Notifications Manager] Found existing chat notification [${notification.id}]" "[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]") Log.i("[Notifications Manager] Notifying [$id] with tag [$tag]")
try {
notificationManager.notify(tag, id, notification) 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) { fun cancel(id: Int, tag: String? = null) {
@ -460,24 +487,23 @@ class NotificationsManager(private val context: Context) {
fun startForeground(coreService: CoreService, useAutoStartDescription: Boolean = true) { fun startForeground(coreService: CoreService, useAutoStartDescription: Boolean = true) {
service = coreService service = coreService
if (serviceNotification == null) { val notification = serviceNotification ?: createServiceNotification(useAutoStartDescription)
createServiceNotification(useAutoStartDescription) if (notification == null) {
if (serviceNotification == null) {
Log.e( Log.e(
"[Notifications Manager] Failed to create service notification, aborting foreground service!" "[Notifications Manager] Failed to create service notification, aborting foreground service!"
) )
return return
} }
}
currentForegroundServiceNotificationId = SERVICE_NOTIF_ID currentForegroundServiceNotificationId = SERVICE_NOTIF_ID
Log.i( Log.i(
"[Notifications Manager] Starting service as foreground [$currentForegroundServiceNotificationId]" "[Notifications Manager] Starting service as foreground [$currentForegroundServiceNotificationId]"
) )
Compatibility.startForegroundService(
Compatibility.startDataSyncForegroundService(
coreService, coreService,
currentForegroundServiceNotificationId, currentForegroundServiceNotificationId,
serviceNotification notification
) )
} }
@ -491,7 +517,7 @@ class NotificationsManager(private val context: Context) {
val coreService = service val coreService = service
if (coreService != null) { if (coreService != null) {
Compatibility.startForegroundService( Compatibility.startCallForegroundService(
coreService, coreService,
currentForegroundServiceNotificationId, currentForegroundServiceNotificationId,
callNotification callNotification
@ -552,11 +578,11 @@ class NotificationsManager(private val context: Context) {
service = null 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) val serviceChannel = context.getString(R.string.notification_channel_service_id)
if (Compatibility.getChannelImportance(notificationManager, serviceChannel) == NotificationManagerCompat.IMPORTANCE_NONE) { if (Compatibility.getChannelImportance(notificationManager, serviceChannel) == NotificationManagerCompat.IMPORTANCE_NONE) {
Log.w("[Notifications Manager] Service channel is disabled!") Log.w("[Notifications Manager] Service channel is disabled!")
return return null
} }
val pendingIntent = NavDeepLinkBuilder(context) val pendingIntent = NavDeepLinkBuilder(context)
@ -588,7 +614,9 @@ class NotificationsManager(private val context: Context) {
builder.setContentIntent(pendingIntent) builder.setContentIntent(pendingIntent)
} }
serviceNotification = builder.build() val notif = builder.build()
serviceNotification = notif
return notif
} }
/* Call related */ /* Call related */
@ -846,14 +874,18 @@ class NotificationsManager(private val context: Context) {
notify(notifiable.notificationId, notification, CHAT_TAG) notify(notifiable.notificationId, notification, CHAT_TAG)
} }
private fun createChatNotifiable(room: ChatRoom, messages: Array<out ChatMessage>): Notifiable { private fun updateChatNotifiableWithMessages(
val notifiable = getNotifiableForRoom(room) notifiable: Notifiable,
room: ChatRoom,
messages: Array<out ChatMessage>
): Boolean {
var updated = false
for (message in messages) { for (message in messages) {
if (message.isRead || message.isOutgoing) continue if (message.isRead || message.isOutgoing) continue
val friend = coreContext.contactsManager.findContactByAddress(message.fromAddress) val friend = coreContext.contactsManager.findContactByAddress(message.fromAddress)
val notifiableMessage = getNotifiableMessage(message, friend) val notifiableMessage = getNotifiableMessage(message, friend)
notifiable.messages.add(notifiableMessage) notifiable.messages.add(notifiableMessage)
updated = true
} }
if (room.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) { if (room.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) {
@ -862,7 +894,7 @@ class NotificationsManager(private val context: Context) {
notifiable.isGroup = true notifiable.isGroup = true
notifiable.groupTitle = room.subject notifiable.groupTitle = room.subject
} }
return notifiable return updated
} }
private fun createChatReactionNotifiable( private fun createChatReactionNotifiable(

View file

@ -44,6 +44,10 @@ import org.linphone.core.tools.Log
* Various utility methods for application * Various utility methods for application
*/ */
class AppUtils { class AppUtils {
interface KeyboardVisibilityListener {
fun onKeyboardVisibilityChanged(visible: Boolean)
}
companion object { companion object {
private val emojiCompat: EmojiCompat? private val emojiCompat: EmojiCompat?
get() = initEmojiCompat() get() = initEmojiCompat()

View file

@ -73,7 +73,7 @@ fun View.hideKeyboard() {
} catch (_: Exception) {} } catch (_: Exception) {}
} }
fun View.addKeyboardInsetListener(lambda: (visible: Boolean) -> Unit) { fun View.setKeyboardInsetListener(lambda: (visible: Boolean) -> Unit) {
doOnLayout { doOnLayout {
var isKeyboardVisible = ViewCompat.getRootWindowInsets(this)?.isVisible( var isKeyboardVisible = ViewCompat.getRootWindowInsets(this)?.isVisible(
WindowInsetsCompat.Type.ime() WindowInsetsCompat.Type.ime()
@ -81,8 +81,9 @@ fun View.addKeyboardInsetListener(lambda: (visible: Boolean) -> Unit) {
lambda(isKeyboardVisible) lambda(isKeyboardVisible)
// See https://issuetracker.google.com/issues/281942480
ViewCompat.setOnApplyWindowInsetsListener( ViewCompat.setOnApplyWindowInsetsListener(
this rootView
) { view, insets -> ) { view, insets ->
val keyboardVisibilityChanged = ViewCompat.getRootWindowInsets(view) val keyboardVisibilityChanged = ViewCompat.getRootWindowInsets(view)
?.isVisible(WindowInsetsCompat.Type.ime()) == true ?.isVisible(WindowInsetsCompat.Type.ime()) == true
@ -90,7 +91,7 @@ fun View.addKeyboardInsetListener(lambda: (visible: Boolean) -> Unit) {
isKeyboardVisible = keyboardVisibilityChanged isKeyboardVisible = keyboardVisibilityChanged
lambda(isKeyboardVisible) lambda(isKeyboardVisible)
} }
insets ViewCompat.onApplyWindowInsets(view, insets)
} }
} }
} }
@ -343,7 +344,7 @@ fun setImageViewScaleType(imageView: ImageView, scaleType: ImageView.ScaleType)
@BindingAdapter("coilRounded") @BindingAdapter("coilRounded")
fun loadRoundImageWithCoil(imageView: ImageView, path: String?) { fun loadRoundImageWithCoil(imageView: ImageView, path: String?) {
if (path != null && path.isNotEmpty() && FileUtils.isExtensionImage(path)) { if (!path.isNullOrEmpty() && FileUtils.isExtensionImage(path)) {
imageView.load(path) { imageView.load(path) {
transformations(CircleCropTransformation()) transformations(CircleCropTransformation())
} }
@ -354,7 +355,7 @@ fun loadRoundImageWithCoil(imageView: ImageView, path: String?) {
@BindingAdapter("coil") @BindingAdapter("coil")
fun loadImageWithCoil(imageView: ImageView, path: String?) { 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)) { if (corePreferences.vfsEnabled && path.startsWith(corePreferences.vfsCachePath)) {
imageView.load(path) { imageView.load(path) {
diskCachePolicy(CachePolicy.DISABLED) diskCachePolicy(CachePolicy.DISABLED)
@ -551,9 +552,23 @@ fun loadAvatarWithCoil(imageView: ImageView, path: String?) {
@BindingAdapter("coilVideoPreview") @BindingAdapter("coilVideoPreview")
fun loadVideoPreview(imageView: ImageView, path: String?) { fun loadVideoPreview(imageView: ImageView, path: String?) {
if (path != null && path.isNotEmpty() && FileUtils.isExtensionVideo(path)) { if (!path.isNullOrEmpty() && FileUtils.isExtensionVideo(path)) {
imageView.load(path) { imageView.load(path) {
videoFrameMillis(0) 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) { fun addPrefixEditTextValidation(editText: EditText, enabled: Boolean) {
if (!enabled) return if (!enabled) return
editText.addTextChangedListener(object : TextWatcher { 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) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { 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") editText.setText("+$s")
} }
} }

View file

@ -26,7 +26,10 @@ import android.content.Intent
import android.database.CursorIndexOutOfBoundsException import android.database.CursorIndexOutOfBoundsException
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
import android.os.ParcelFileDescriptor
import android.os.Process.myUid
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.system.Os.fstat
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import java.io.* import java.io.*
@ -42,6 +45,15 @@ import org.linphone.R
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
class FileUtils { class FileUtils {
enum class MimeType {
PlainText,
Pdf,
Image,
Video,
Audio,
Unknown
}
companion object { companion object {
fun getNameFromFilePath(filePath: String): String { fun getNameFromFilePath(filePath: String): String {
var name = filePath var name = filePath
@ -64,36 +76,28 @@ class FileUtils {
return extension.lowercase(Locale.getDefault()) return extension.lowercase(Locale.getDefault())
} }
fun isMimePlainText(type: String?): Boolean { fun getMimeType(type: String?): MimeType {
return type?.startsWith("text/plain") ?: false 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 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 isExtensionImage(path: String): Boolean { fun isExtensionImage(path: String): Boolean {
val extension = getExtensionFromFileName(path) val extension = getExtensionFromFileName(path)
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
return isMimeImage(type) return getMimeType(type) == MimeType.Image
} }
fun isExtensionVideo(path: String): Boolean { fun isExtensionVideo(path: String): Boolean {
val extension = getExtensionFromFileName(path) val extension = getExtensionFromFileName(path)
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
return isMimeVideo(type) return getMimeType(type) == MimeType.Video
} }
fun clearExistingPlainFiles() { fun clearExistingPlainFiles() {
@ -269,6 +273,21 @@ class FileUtils {
var result: String? = null var result: String? = null
val name: String = getNameFromUri(uri, context) 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 { try {
val localFile: File = createFile(name) val localFile: File = createFile(name)
val remoteFile = val remoteFile =

View file

@ -51,7 +51,7 @@ class LinphoneUtils {
} }
val localDisplayName = account?.params?.identityAddress?.displayName val localDisplayName = account?.params?.identityAddress?.displayName
// Do not return an empty local display name // Do not return an empty local display name
if (localDisplayName != null && localDisplayName.isNotEmpty()) { if (!localDisplayName.isNullOrEmpty()) {
return localDisplayName return localDisplayName
} }
} }
@ -179,7 +179,7 @@ class LinphoneUtils {
fun deleteFilesAttachedToChatMessage(chatMessage: ChatMessage) { fun deleteFilesAttachedToChatMessage(chatMessage: ChatMessage) {
for (content in chatMessage.contents) { for (content in chatMessage.contents) {
val filePath = content.filePath val filePath = content.filePath
if (filePath != null && filePath.isNotEmpty()) { if (!filePath.isNullOrEmpty()) {
Log.i("[Linphone Utils] Deleting file $filePath") Log.i("[Linphone Utils] Deleting file $filePath")
FileUtils.deleteFile(filePath) FileUtils.deleteFile(filePath)
} }
@ -288,6 +288,11 @@ class LinphoneUtils {
return true return true
} }
fun isFileTransferAvailable(): Boolean {
val core = coreContext.core
return core.fileTransferServer.orEmpty().isNotEmpty()
}
fun hashPassword( fun hashPassword(
userId: String, userId: String,
password: String, password: String,

View file

@ -181,13 +181,13 @@
android:contentDescription="@null" android:contentDescription="@null"
coilVoipContact="@{conferenceViewModel.speakingParticipant}" coilVoipContact="@{conferenceViewModel.speakingParticipant}"
android:background="@drawable/generated_avatar_bg" android:background="@drawable/generated_avatar_bg"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintBottom_toBottomOf="@id/active_speaker_background" app:layout_constraintBottom_toBottomOf="@id/active_speaker_background"
app:layout_constraintEnd_toEndOf="@id/active_speaker_background" app:layout_constraintEnd_toEndOf="@id/active_speaker_background"
app:layout_constraintHeight_max="@dimen/voip_contact_avatar_max_size" app:layout_constraintHeight_max="@dimen/voip_contact_avatar_max_size"
app:layout_constraintStart_toStartOf="@id/active_speaker_background" app:layout_constraintStart_toStartOf="@id/active_speaker_background"
app:layout_constraintTop_toTopOf="@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" />
<ImageView <ImageView
android:id="@+id/speaking_participant_paused" android:id="@+id/speaking_participant_paused"

View file

@ -119,6 +119,7 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
assistantPhoneNumberPrefixValidation="@{true}" assistantPhoneNumberPrefixValidation="@{true}"
errorMessage="@={viewModel.prefixError}"
android:text="@={viewModel.prefix}" android:text="@={viewModel.prefix}"
android:imeOptions="actionNext" android:imeOptions="actionNext"
android:singleLine="true" android:singleLine="true"

View file

@ -4,8 +4,7 @@
android:layout_height="40dp" android:layout_height="40dp"
android:layout_gravity="center" android:layout_gravity="center"
android:orientation="horizontal" android:orientation="horizontal"
android:padding="5dp" android:padding="5dp">
android:background="?attr/backgroundColor">
<TextView <TextView
android:id="@+id/country_name" android:id="@+id/country_name"

View file

@ -116,6 +116,7 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
assistantPhoneNumberPrefixValidation="@{true}" assistantPhoneNumberPrefixValidation="@{true}"
errorMessage="@={viewModel.prefixError}"
android:text="@={viewModel.prefix}" android:text="@={viewModel.prefix}"
android:imeOptions="actionNext" android:imeOptions="actionNext"
android:singleLine="true" android:singleLine="true"

View file

@ -126,6 +126,7 @@
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
assistantPhoneNumberPrefixValidation="@{true}" assistantPhoneNumberPrefixValidation="@{true}"
errorMessage="@={viewModel.prefixError}"
android:text="@={viewModel.prefix}" android:text="@={viewModel.prefix}"
android:imeOptions="actionNext" android:imeOptions="actionNext"
android:singleLine="true" android:singleLine="true"

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <layout xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data> <data>
<import type="android.view.View"/> <import type="android.view.View"/>
@ -14,8 +16,15 @@
android:layout_margin="5dp" android:layout_margin="5dp"
android:orientation="vertical"> android:orientation="vertical">
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:background="?attr/backgroundColor"/>
<ImageView <ImageView
android:visibility="@{data.image ? View.VISIBLE : View.GONE}" android:visibility="@{data.image ? View.VISIBLE : View.GONE, default=gone}"
android:contentDescription="@string/content_description_pending_file_transfer" android:contentDescription="@string/content_description_pending_file_transfer"
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="100dp" android:layout_height="100dp"
@ -25,8 +34,25 @@
android:scaleType="centerCrop" android:scaleType="centerCrop"
coil="@{data.path}"/> coil="@{data.path}"/>
<TextView
android:visibility="@{data.video ? View.VISIBLE : View.GONE, default=gone}"
style="@style/chat_file_attachment_font"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="5dp"
android:layout_marginRight="10dp"
android:ellipsize="middle"
android:singleLine="true"
android:padding="10dp"
android:gravity="center"
android:textAlignment="center"
android:drawablePadding="5dp"
android:text="@{data.fileName, default=`test.mkv`}"
app:drawableTopCompat="@drawable/file_video" />
<ImageView <ImageView
android:visibility="@{data.video ? View.VISIBLE : View.GONE}" android:visibility="@{data.video ? View.VISIBLE : View.GONE, default=gone}"
android:contentDescription="@string/content_description_pending_file_transfer" android:contentDescription="@string/content_description_pending_file_transfer"
android:layout_width="100dp" android:layout_width="100dp"
android:layout_height="100dp" android:layout_height="100dp"
@ -36,41 +62,23 @@
android:scaleType="centerCrop" android:scaleType="centerCrop"
coilVideoPreview="@{data.path}"/> coilVideoPreview="@{data.path}"/>
<ImageView
android:visibility="@{data.video ? View.VISIBLE : View.GONE, default=gone}"
android:layout_width="@dimen/play_pause_button_size"
android:layout_height="@dimen/play_pause_button_size"
android:padding="9dp"
android:src="@drawable/record_play_dark"
android:background="@drawable/round_recording_button_background_dark"
android:contentDescription="@string/content_description_chat_message_video_attachment"
android:layout_centerInParent="true"/>
<LinearLayout
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:orientation="vertical"
android:gravity="center"
android:background="?attr/backgroundColor"
android:visibility="@{data.image || data.video ? View.GONE : View.VISIBLE}">
<TextView <TextView
style="@style/chat_file_attachment_font" style="@style/chat_file_attachment_font"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="5dp" android:layout_alignParentBottom="true"
android:layout_marginBottom="5dp"
android:layout_marginRight="10dp"
android:ellipsize="middle" android:ellipsize="middle"
android:singleLine="true" android:singleLine="true"
android:padding="10dp" android:padding="10dp"
android:gravity="center" android:gravity="center"
android:textAlignment="center" android:textAlignment="center"
android:drawablePadding="5dp" 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}" android:drawableTop="@{data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : @drawable/file), default=@drawable/file}"
android:text="@{data.fileName, default=`test.txt`}"/> tools:ignore="UseCompatTextViewDrawableXml" />
</LinearLayout>
<ImageView <ImageView
android:onClick="@{() -> data.delete()}" android:onClick="@{() -> data.delete()}"

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <layout xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data> <data>
<import type="android.view.View" /> <import type="android.view.View" />
@ -29,7 +31,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="5dp" 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:drawablePadding="5dp"
android:ellipsize="middle" android:ellipsize="middle"
android:gravity="center" android:gravity="center"
@ -37,7 +38,9 @@
android:onLongClick="@{longClickListener}" android:onLongClick="@{longClickListener}"
android:singleLine="true" android:singleLine="true"
android:text="@{data.fileName, default=`test.pdf`}" 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" />
<TextView <TextView
style="@style/chat_file_attachment_font" style="@style/chat_file_attachment_font"

View file

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <layout xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data> <data>
<import type="android.view.View" /> <import type="android.view.View" />
@ -15,6 +17,8 @@
</data> </data>
<RelativeLayout <RelativeLayout
android:onClick="@{() -> data.openFile()}"
android:onLongClick="@{longClickListener}"
android:layout_width="@dimen/chat_message_bubble_file_size" android:layout_width="@dimen/chat_message_bubble_file_size"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="@dimen/chat_message_bubble_file_size" android:minHeight="@dimen/chat_message_bubble_file_size"
@ -29,15 +33,14 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="5dp" 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:drawablePadding="5dp"
android:ellipsize="middle" android:ellipsize="middle"
android:gravity="center" android:gravity="center"
android:onClick="@{() -> data.openFile()}"
android:onLongClick="@{longClickListener}"
android:singleLine="true" android:singleLine="true"
android:text="@{data.fileName, default=`test.pdf`}" 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" />
</RelativeLayout> </RelativeLayout>

View file

@ -18,14 +18,14 @@
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:onClick="@{() -> data.openFile()}"
android:onLongClick="@{longClickListener}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="@{inflatedVisibility}" android:visibility="@{inflatedVisibility}"
inflatedLifecycleOwner="@{true}"> inflatedLifecycleOwner="@{true}">
<ImageView <ImageView
android:onClick="@{() -> data.openFile()}"
android:onLongClick="@{longClickListener}"
android:contentDescription="@string/content_description_downloaded_file_transfer" android:contentDescription="@string/content_description_downloaded_file_transfer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View file

@ -85,7 +85,8 @@
android:background="@drawable/led_background" android:background="@drawable/led_background"
android:padding="2dp" android:padding="2dp"
app:presenceIcon="@{data.presenceStatus}" app:presenceIcon="@{data.presenceStatus}"
android:visibility="@{data.chatMessage.outgoing || selectionListViewModel.isEditionEnabled || data.hideAvatar || data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" /> android:visibility="@{data.chatMessage.outgoing || selectionListViewModel.isEditionEnabled || data.hideAvatar || data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}"
tools:ignore="ContentDescription" />
<LinearLayout <LinearLayout
android:id="@+id/background" android:id="@+id/background"

View file

@ -26,6 +26,15 @@
android:scaleType="@{ScaleType.CENTER_CROP}" android:scaleType="@{ScaleType.CENTER_CROP}"
android:adjustViewBounds="true" /> android:adjustViewBounds="true" />
<ImageView
android:layout_width="@dimen/chat_message_small_bubble_file_size"
android:layout_height="@dimen/chat_message_small_bubble_file_size"
android:background="@drawable/chat_bubble_reply_file_background"
android:contentDescription="@{data.fileName}"
android:padding="10dp"
android:src="@drawable/file_video"
android:visibility="@{data.video ? View.VISIBLE : View.GONE, default=gone}" />
<ImageView <ImageView
android:contentDescription="@string/content_description_downloaded_file_transfer" android:contentDescription="@string/content_description_downloaded_file_transfer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -37,16 +46,6 @@
android:scaleType="@{ScaleType.CENTER_CROP}" android:scaleType="@{ScaleType.CENTER_CROP}"
android:adjustViewBounds="true" /> android:adjustViewBounds="true" />
<ImageView
android:visibility="@{data.video ? View.VISIBLE : View.GONE, default=gone}"
android:layout_width="@dimen/play_pause_button_size"
android:layout_height="@dimen/play_pause_button_size"
android:padding="9dp"
android:src="@drawable/record_play_dark"
android:background="@drawable/round_recording_button_background_dark"
android:contentDescription="@string/content_description_chat_message_video_attachment"
android:layout_centerInParent="true"/>
<TextView <TextView
android:visibility="@{data.isVoiceRecording ? View.VISIBLE : View.GONE, default=gone}" android:visibility="@{data.isVoiceRecording ? View.VISIBLE : View.GONE, default=gone}"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -24,6 +24,16 @@
android:scaleType="@{ScaleType.CENTER_CROP}" android:scaleType="@{ScaleType.CENTER_CROP}"
android:adjustViewBounds="true" /> android:adjustViewBounds="true" />
<ImageView
android:layout_width="@dimen/chat_message_small_bubble_file_size"
android:layout_height="@dimen/chat_message_small_bubble_file_size"
android:layout_margin="5dp"
android:background="@drawable/chat_bubble_reply_file_background"
android:contentDescription="@{data.fileName}"
android:padding="10dp"
android:src="@drawable/file_video"
android:visibility="@{data.video ? View.VISIBLE : View.GONE, default=gone}" />
<ImageView <ImageView
android:contentDescription="@string/content_description_downloaded_file_transfer" android:contentDescription="@string/content_description_downloaded_file_transfer"
android:layout_width="@dimen/chat_message_small_bubble_file_size" android:layout_width="@dimen/chat_message_small_bubble_file_size"
@ -34,17 +44,6 @@
android:scaleType="@{ScaleType.CENTER_CROP}" android:scaleType="@{ScaleType.CENTER_CROP}"
android:adjustViewBounds="true" /> android:adjustViewBounds="true" />
<ImageView
android:visibility="@{data.video ? View.VISIBLE : View.GONE, default=gone}"
android:layout_width="@dimen/play_pause_button_size"
android:layout_height="@dimen/play_pause_button_size"
android:layout_margin="5dp"
android:padding="9dp"
android:src="@drawable/record_play_dark"
android:background="@drawable/round_recording_button_background_dark"
android:contentDescription="@string/content_description_chat_message_video_attachment"
android:layout_centerInParent="true"/>
<TextView <TextView
android:visibility="@{data.isVoiceRecording ? View.VISIBLE : View.GONE, default=gone}" android:visibility="@{data.isVoiceRecording ? View.VISIBLE : View.GONE, default=gone}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -74,10 +73,7 @@
<ImageView <ImageView
android:layout_width="@dimen/chat_message_small_bubble_file_size" android:layout_width="@dimen/chat_message_small_bubble_file_size"
android:layout_height="@dimen/chat_message_small_bubble_file_size" android:layout_height="@dimen/chat_message_small_bubble_file_size"
android:layout_marginStart="5dp" android:layout_margin="5dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="5dp"
android:background="@drawable/chat_bubble_reply_file_background" android:background="@drawable/chat_bubble_reply_file_background"
android:contentDescription="@{data.fileName}" android:contentDescription="@{data.fileName}"
android:padding="10dp" android:padding="10dp"

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <layout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<data> <data>
<import type="android.view.View" /> <import type="android.view.View" />
@ -16,14 +17,29 @@
</data> </data>
<RelativeLayout <RelativeLayout
android:onClick="@{() -> data.openFile()}"
android:onLongClick="@{longClickListener}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="@{inflatedVisibility}" android:visibility="@{inflatedVisibility}"
inflatedLifecycleOwner="@{true}"> inflatedLifecycleOwner="@{true}"
android:background="?attr/backgroundColor">
<TextView
style="@style/chat_file_attachment_font"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:ellipsize="middle"
android:singleLine="true"
android:padding="10dp"
android:gravity="center"
android:textAlignment="center"
android:drawablePadding="5dp"
android:text="@{data.fileName, default=`test.mkv`}"
app:drawableTopCompat="@drawable/file_video" />
<ImageView <ImageView
android:onClick="@{() -> data.openFile()}"
android:onLongClick="@{longClickListener}"
android:contentDescription="@string/content_description_downloaded_file_transfer" android:contentDescription="@string/content_description_downloaded_file_transfer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -33,15 +49,6 @@
android:scaleType="@{data.alone ? ScaleType.FIT_CENTER : ScaleType.CENTER_CROP}" android:scaleType="@{data.alone ? ScaleType.FIT_CENTER : ScaleType.CENTER_CROP}"
android:adjustViewBounds="true" /> android:adjustViewBounds="true" />
<ImageView
android:layout_width="@dimen/play_pause_button_size"
android:layout_height="@dimen/play_pause_button_size"
android:padding="9dp"
android:src="@drawable/record_play_dark"
android:background="@drawable/round_recording_button_background_dark"
android:contentDescription="@string/content_description_chat_message_video_attachment"
android:layout_centerInParent="true"/>
</RelativeLayout> </RelativeLayout>
</layout> </layout>

View file

@ -163,6 +163,7 @@
android:layout_alignRight="@id/searchBar" android:layout_alignRight="@id/searchBar"
android:layout_alignBottom="@id/searchBar" android:layout_alignBottom="@id/searchBar"
android:onClick="@{() -> viewModel.clearFilter()}" android:onClick="@{() -> viewModel.clearFilter()}"
android:contentDescription="@string/content_description_clear_field"
android:src="@drawable/field_clean" /> android:src="@drawable/field_clean" />
<View <View

View file

@ -44,6 +44,7 @@
<RelativeLayout <RelativeLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/backgroundColor"
android:clickable="true"> android:clickable="true">
<LinearLayout <LinearLayout

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data> <data>
<import type="android.view.View"/> <import type="android.view.View"/>
@ -49,7 +50,8 @@
android:background="@drawable/led_background" android:background="@drawable/led_background"
android:padding="@dimen/contact_presence_badge_padding" android:padding="@dimen/contact_presence_badge_padding"
app:presenceIcon="@{data.presenceStatus}" 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" />
<ImageView <ImageView
android:layout_width="20dp" android:layout_width="20dp"

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data> <data>
<import type="android.view.View"/> <import type="android.view.View"/>
@ -50,7 +51,8 @@
android:background="@drawable/led_background" android:background="@drawable/led_background"
android:padding="@dimen/contact_presence_badge_padding" android:padding="@dimen/contact_presence_badge_padding"
android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}"
app:presenceIcon="@{data.presenceStatus}" /> app:presenceIcon="@{data.presenceStatus}"
tools:ignore="ContentDescription" />
<ImageView <ImageView
android:layout_width="20dp" android:layout_width="20dp"

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data> <data>
<import type="android.view.View"/> <import type="android.view.View"/>
@ -37,7 +38,8 @@
android:background="@drawable/led_background" android:background="@drawable/led_background"
android:padding="@dimen/contact_presence_badge_padding" android:padding="@dimen/contact_presence_badge_padding"
app:presenceIcon="@{data.presenceStatus}" 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" />
<TextView <TextView
android:id="@+id/time" android:id="@+id/time"

View file

@ -62,7 +62,8 @@
app:presenceIcon="@{data.presenceStatus}" 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}"
app:layout_constraintStart_toStartOf="@id/avatar" app:layout_constraintStart_toStartOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"/> app:layout_constraintBottom_toBottomOf="@id/avatar"
tools:ignore="ContentDescription"/>
<ImageView <ImageView
android:id="@+id/securityLevel" android:id="@+id/securityLevel"

View file

@ -34,7 +34,8 @@
<RelativeLayout <RelativeLayout
android:layout_width="@dimen/master_fragment_width" android:layout_width="@dimen/master_fragment_width"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start"> android:layout_gravity="start"
android:background="?attr/backgroundColor">
<LinearLayout <LinearLayout
android:id="@+id/top_bar" android:id="@+id/top_bar"

View file

@ -125,9 +125,8 @@
android:layout_width="@dimen/chat_message_sending_icons_size" android:layout_width="@dimen/chat_message_sending_icons_size"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginStart="@dimen/chat_message_sending_icons_margin" android:layout_marginStart="@dimen/chat_message_sending_icons_margin"
android:layout_marginEnd="@dimen/chat_message_sending_icons_margin"
android:contentDescription="@string/content_description_attach_file" android:contentDescription="@string/content_description_attach_file"
android:enabled="@{chatSendingViewModel.attachFileEnabled &amp;&amp; !chatSendingViewModel.attachFilePending}" android:enabled="@{chatSendingViewModel.attachFileEnabled &amp;&amp; !chatSendingViewModel.attachFilePending &amp;&amp; chatSendingViewModel.isFileTransferAvailable}"
android:onClick="@{attachFileClickListener}" android:onClick="@{attachFileClickListener}"
android:paddingTop="@dimen/chat_message_sending_icons_margin" android:paddingTop="@dimen/chat_message_sending_icons_margin"
android:paddingBottom="@dimen/chat_message_sending_icons_margin" android:paddingBottom="@dimen/chat_message_sending_icons_margin"
@ -142,13 +141,13 @@
android:layout_width="@dimen/chat_message_sending_icons_size" android:layout_width="@dimen/chat_message_sending_icons_size"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginStart="@dimen/chat_message_sending_icons_margin" android:layout_marginStart="@dimen/chat_message_sending_icons_margin"
android:layout_marginEnd="@dimen/chat_message_sending_icons_margin"
android:contentDescription="@string/content_description_voice_recording" android:contentDescription="@string/content_description_voice_recording"
android:onClick="@{() -> chatSendingViewModel.toggleVoiceRecording()}" android:onClick="@{() -> chatSendingViewModel.toggleVoiceRecording()}"
android:onTouch="@{voiceRecordingTouchListener}" android:onTouch="@{voiceRecordingTouchListener}"
android:paddingTop="@dimen/chat_message_sending_icons_margin" android:paddingTop="@dimen/chat_message_sending_icons_margin"
android:paddingBottom="@dimen/chat_message_sending_icons_margin" android:paddingBottom="@dimen/chat_message_sending_icons_margin"
android:selected="@{chatSendingViewModel.isVoiceRecording}" android:selected="@{chatSendingViewModel.isVoiceRecording}"
android:enabled="@{chatSendingViewModel.isFileTransferAvailable}"
android:src="@drawable/record_audio_message" android:src="@drawable/record_audio_message"
app:layout_constraintHeight_max="@dimen/chat_message_sending_icons_size" app:layout_constraintHeight_max="@dimen/chat_message_sending_icons_size"
app:layout_constraintBottom_toBottomOf="@id/message" app:layout_constraintBottom_toBottomOf="@id/message"
@ -162,6 +161,7 @@
android:layout_below="@id/emoji_picker" android:layout_below="@id/emoji_picker"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/chat_message_sending_icons_margin" android:layout_marginStart="@dimen/chat_message_sending_icons_margin"
android:layout_marginEnd="@{chatSendingViewModel.isEmojiPickerVisible ? @dimen/margin_0dp : @dimen/chat_message_sending_icons_margin}"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:layout_marginBottom="10dp" android:layout_marginBottom="10dp"
android:background="@color/header_background_color" android:background="@color/header_background_color"
@ -174,37 +174,21 @@
android:textColor="@color/black_color" android:textColor="@color/black_color"
android:textCursorDrawable="@null" android:textCursorDrawable="@null"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/message_right_barrier" app:layout_constraintEnd_toStartOf="@id/emoji_picker_toggle"
app:layout_constraintStart_toEndOf="@id/voice_record" app:layout_constraintStart_toEndOf="@id/voice_record"
app:layout_constraintTop_toBottomOf="@id/emoji_picker" /> app:layout_constraintTop_toBottomOf="@id/emoji_picker" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/message_right_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="left"
app:constraint_referenced_ids="send_message, emoji_picker_toggle"/>
<View
android:id="@+id/emoji_picker_bg"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/header_background_color"
app:layout_constraintBottom_toBottomOf="@id/message"
app:layout_constraintStart_toStartOf="@id/emoji_picker_toggle"
app:layout_constraintEnd_toEndOf="@id/emoji_picker_toggle"
app:layout_constraintTop_toTopOf="@id/message" />
<ImageView <ImageView
android:id="@+id/emoji_picker_toggle" android:id="@+id/emoji_picker_toggle"
android:layout_width="@dimen/chat_message_sending_icons_size" android:layout_width="@dimen/chat_message_sending_icons_size"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginStart="@dimen/chat_message_sending_icons_margin"
android:layout_marginEnd="@dimen/chat_message_sending_icons_margin" android:layout_marginEnd="@dimen/chat_message_sending_icons_margin"
android:onClick="@{() -> chatSendingViewModel.toggleEmojiPicker()}" android:onClick="@{() -> chatSendingViewModel.toggleEmojiPicker()}"
android:paddingTop="@dimen/chat_message_sending_icons_margin" android:paddingTop="@dimen/chat_message_sending_icons_margin"
android:paddingBottom="@dimen/chat_message_sending_icons_margin" android:paddingBottom="@dimen/chat_message_sending_icons_margin"
android:src="@drawable/emoji" android:src="@drawable/emoji"
android:background="@color/header_background_color"
android:contentDescription="@string/content_description_emoji_picker"
android:selected="@{chatSendingViewModel.isEmojiPickerOpen}" android:selected="@{chatSendingViewModel.isEmojiPickerOpen}"
android:visibility="@{chatSendingViewModel.isEmojiPickerVisible ? View.VISIBLE : View.GONE}" android:visibility="@{chatSendingViewModel.isEmojiPickerVisible ? View.VISIBLE : View.GONE}"
app:layout_constraintHeight_max="@dimen/chat_message_sending_icons_size" app:layout_constraintHeight_max="@dimen/chat_message_sending_icons_size"
@ -216,7 +200,6 @@
android:id="@+id/send_message" android:id="@+id/send_message"
android:layout_width="@dimen/chat_message_sending_icons_size" android:layout_width="@dimen/chat_message_sending_icons_size"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginStart="@dimen/chat_message_sending_icons_margin"
android:layout_marginEnd="@dimen/chat_message_sending_icons_margin" android:layout_marginEnd="@dimen/chat_message_sending_icons_margin"
android:contentDescription="@string/content_description_send_message" android:contentDescription="@string/content_description_send_message"
android:enabled="@{chatSendingViewModel.sendMessageEnabled &amp;&amp; !chatSendingViewModel.attachingFileInProgress}" android:enabled="@{chatSendingViewModel.sendMessageEnabled &amp;&amp; !chatSendingViewModel.attachingFileInProgress}"

View file

@ -79,6 +79,7 @@
android:layout_alignRight="@id/searchBar" android:layout_alignRight="@id/searchBar"
android:layout_alignBottom="@id/searchBar" android:layout_alignBottom="@id/searchBar"
android:onClick="@{() -> viewModel.clearFilter()}" android:onClick="@{() -> viewModel.clearFilter()}"
android:contentDescription="@string/content_description_clear_field"
android:src="@drawable/field_clean" /> android:src="@drawable/field_clean" />
</RelativeLayout> </RelativeLayout>

View file

@ -102,6 +102,7 @@
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:layout_marginStart="5dp" android:layout_marginStart="5dp"
android:layout_marginEnd="5dp" android:layout_marginEnd="5dp"
android:inputType="textEmailSubject"
style="@style/conference_scheduling_font" style="@style/conference_scheduling_font"
android:background="?attr/voipFormDisabledFieldBackgroundColor" android:background="?attr/voipFormDisabledFieldBackgroundColor"
android:text="@{viewModel.subject}" android:text="@{viewModel.subject}"
@ -252,6 +253,7 @@
android:gravity="top" android:gravity="top"
android:minLines="3" android:minLines="3"
android:maxLines="5" android:maxLines="5"
android:inputType="textMultiLine"
style="@style/conference_scheduling_font" style="@style/conference_scheduling_font"
android:background="?attr/voipFormDisabledFieldBackgroundColor" android:background="?attr/voipFormDisabledFieldBackgroundColor"
android:hint="@string/conference_schedule_description_hint" android:hint="@string/conference_schedule_description_hint"

View file

@ -105,7 +105,8 @@
android:background="@drawable/led_background" android:background="@drawable/led_background"
android:padding="@dimen/contact_presence_big_badge_padding" android:padding="@dimen/contact_presence_big_badge_padding"
app:presenceIcon="@{viewModel.presenceStatus}" 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" />
</RelativeLayout> </RelativeLayout>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data> <data>
<import type="android.view.View"/> <import type="android.view.View"/>
@ -54,7 +55,8 @@
android:background="@drawable/led_background" android:background="@drawable/led_background"
android:padding="@dimen/contact_presence_badge_padding" android:padding="@dimen/contact_presence_badge_padding"
app:presenceIcon="@{viewModel.presenceStatus}" 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" />
<LinearLayout <LinearLayout
android:id="@+id/right" android:id="@+id/right"
@ -72,7 +74,7 @@
android:src="@drawable/linphone_logo_tinted" android:src="@drawable/linphone_logo_tinted"
android:layout_marginRight="10dp" android:layout_marginRight="10dp"
android:contentDescription="@string/content_description_linphone_user" android:contentDescription="@string/content_description_linphone_user"
android:visibility="@{viewModel.hasPresence() ? View.VISIBLE : View.GONE}" /> android:visibility="@{viewModel.hasLongTermPresence ? View.VISIBLE : View.GONE, default=gone}" />
<CheckBox <CheckBox
android:onClick="@{() -> selectionListViewModel.onToggleSelect(position)}" android:onClick="@{() -> selectionListViewModel.onToggleSelect(position)}"

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