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
- Chat messages emoji "reactions"
## [5.1.0] - Unreleased
## [5.1.0] - 2023-08-21
### Added
- Showing short term presence for contacts whom publish it + added setting to disable it (enabled by default for sip.linphone.org accounts)
- Confirmation dialog before removing account
- Attended transfer instead of blind transfer if there is more than 1 call
- Added emoji picker in chat room, and increase size of text if it only contains emojis
- Added hidden setting to disable video completely
- Added hidden setting to prevent adding / editing / removing native contacts
- Added hidden setting to protect settings access using account password
- Last sent message delivery status (IMDN) icon in chat rooms list
- Emoji picker in chat room, and increase size of text if it only contains emojis
- Hidden setting to disable video completely
- Hidden setting to prevent adding / editing / removing native contacts
- Hidden setting to protect settings access using account password
- SIP URI in call can be selected using long press
- Dialog showing up asking for correct account password in case of failed authentication
### Changed
- Switched Account Creator backend from XMLRPC to FlexiAPI, it now requires to be able to receive a push notification
@ -38,6 +40,35 @@ Group changes to describe their impact on the project, as follows:
### Fixed
- Messages not marked as reply in basic chat room if sending more than 1 content
- Chat message video attachment display when failing to get a preview picture
## [5.0.14] - 2023-06-20
### Changed
- SDK update only
## [5.0.13] - 2023-06-15
### Changed
- SDK update only
## [5.0.12] - 2023-05-23
### Fixed
- Crash if notification manager throws an exception
- Video preview not moving if call was started in audio only
## [5.0.11] - 2023-05-09
### Fixed
- Wrong call displayed when hanging up a call while an incoming one is ringing
- Crash related to call history
- Crash due to wrongly format string
- Add/remove missing listener on FriendLists created after Core has been created
### Changed
- Improved GSM call interruption
- Updated translations
## [5.0.11] - 2023-05-09

View file

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

View file

@ -54,6 +54,12 @@
<!-- Needed for foreground service
(https://developer.android.com/guide/components/foreground-services) -->
<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
android:name=".LinphoneApplication"
@ -63,7 +69,6 @@
android:label="${appLabel}"
android:localeConfig="@xml/locales_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:extractNativeLibs="${extractNativeLibs}"
android:theme="@style/AppTheme"
android:allowNativeHeapPointerTagging="false">
@ -121,6 +126,7 @@
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.DIAL" />
<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.BROWSABLE" />
<data android:scheme="tel" />
@ -128,12 +134,6 @@
<data android:scheme="sips" />
<data android:scheme="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" />
</intent-filter>
@ -169,7 +169,14 @@
<service
android:name=".core.CoreService"
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:label="@string/app_name" />

View file

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

View file

@ -83,7 +83,9 @@ abstract class GenericFragment<T : ViewDataBinding> : Fragment() {
}
}
} catch (ise: IllegalStateException) {
Log.e("[Generic Fragment] ${getFragmentRealClassName()} Can't go back: $ise")
Log.e(
"[Generic Fragment] ${getFragmentRealClassName()}.handleOnBackPressed() Can't go back: $ise"
)
}
}
}
@ -137,7 +139,8 @@ abstract class GenericFragment<T : ViewDataBinding> : Fragment() {
try {
requireActivity().onBackPressedDispatcher.onBackPressed()
} catch (ise: IllegalStateException) {
Log.e("[Generic Fragment] ${getFragmentRealClassName()} can't go back: $ise")
Log.w("[Generic Fragment] ${getFragmentRealClassName()}.goBack() can't go back: $ise")
onBackPressedCallback.handleOnBackPressed()
}
}

View file

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

View file

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

View file

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

View file

@ -73,6 +73,10 @@ class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountL
countryPickerFragment.show(childFragmentManager, "CountryPicker")
}
viewModel.prefix.observe(viewLifecycleOwner) { internationalPrefix ->
viewModel.getCountryNameFromPrefix(internationalPrefix)
}
viewModel.goToSmsValidationEvent.observe(
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.navigateToAccountSettings
import org.linphone.activities.navigateToEchoCancellerCalibration
import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantPhoneAccountValidationFragmentBinding
@ -113,7 +114,7 @@ class PhoneAccountValidationFragment : GenericFragment<AssistantPhoneAccountVali
"[Assistant] [Phone Account Validation] Found 4 digits as primary clip in clipboard, using it and clear it"
)
viewModel.code.value = clip
clipboard.clearPrimaryClip()
Compatibility.clearClipboard(clipboard)
}
}
}

View file

@ -21,7 +21,6 @@
package org.linphone.activities.assistant.viewmodels
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.*
import org.linphone.activities.assistant.fragments.CountryPickerFragment
import org.linphone.core.AccountCreator
import org.linphone.core.DialPlan
@ -33,6 +32,7 @@ abstract class AbstractPhoneViewModel(accountCreator: AccountCreator) :
CountryPickerFragment.CountryPickedListener {
val prefix = MutableLiveData<String>()
val prefixError = MutableLiveData<String>()
val phoneNumber = MutableLiveData<String>()
val phoneNumberError = MutableLiveData<String>()
@ -49,7 +49,10 @@ abstract class AbstractPhoneViewModel(accountCreator: AccountCreator) :
}
fun isPhoneNumberOk(): Boolean {
return prefix.value.orEmpty().isNotEmpty() && phoneNumber.value.orEmpty().isNotEmpty() && phoneNumberError.value.orEmpty().isEmpty()
return prefix.value.orEmpty().length > 1 && // Not just '+' character
prefixError.value.orEmpty().isEmpty() &&
phoneNumber.value.orEmpty().isNotEmpty() &&
phoneNumberError.value.orEmpty().isEmpty()
}
fun updateFromPhoneNumberAndOrDialPlan(number: String?, dialPlan: DialPlan?) {
@ -70,7 +73,7 @@ abstract class AbstractPhoneViewModel(accountCreator: AccountCreator) :
}
}
private fun getCountryNameFromPrefix(prefix: String?) {
fun getCountryNameFromPrefix(prefix: String?) {
if (!prefix.isNullOrEmpty()) {
val countryCode = if (prefix.first() == '+') prefix.substring(1) else prefix
val dialPlan = PhoneNumberUtils.getDialPlanFromCountryCallingPrefix(countryCode)

View file

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

View file

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

View file

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

View file

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

View file

@ -108,10 +108,19 @@ class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewMo
fun removeInvalidProxyConfig() {
val account = accountToCheck
account ?: return
val core = coreContext.core
val authInfo = account.findAuthInfo()
if (authInfo != null) coreContext.core.removeAuthInfo(authInfo)
coreContext.core.removeAccount(account)
if (authInfo != null) core.removeAuthInfo(authInfo)
core.removeAccount(account)
accountToCheck = null
// Make sure there is a valid default account
val accounts = core.accountList
if (accounts.isNotEmpty() && core.defaultAccount == null) {
core.defaultAccount = accounts.first()
core.refreshRegisters()
}
}
fun continueEvenIfInvalidCredentials() {
@ -143,6 +152,8 @@ class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewMo
}
private fun isLoginButtonEnabled(): Boolean {
return username.value.orEmpty().isNotEmpty() && domain.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty()
return username.value.orEmpty().isNotEmpty() &&
domain.value.orEmpty().isNotEmpty() &&
password.value.orEmpty().isNotEmpty()
}
}

View file

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

View file

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

View file

@ -34,7 +34,7 @@ class QrCodeViewModel : ViewModel() {
private val listener = object : CoreListenerStub() {
override fun onQrcodeFound(core: Core, result: String?) {
Log.i("[QR Code] Found [$result]")
Log.i("[Assistant] [QR Code] Found [$result]")
if (result != null) qrCodeFoundEvent.postValue(Event(result))
}
}
@ -54,7 +54,7 @@ class QrCodeViewModel : ViewModel() {
for (camera in coreContext.core.videoDevicesList) {
if (camera.contains("Back")) {
Log.i("[QR Code] Found back facing camera: $camera")
Log.i("[Assistant] [QR Code] Found back facing camera: $camera")
coreContext.core.videoDevice = camera
return
}
@ -62,7 +62,7 @@ class QrCodeViewModel : ViewModel() {
val first = coreContext.core.videoDevicesList.firstOrNull()
if (first != null) {
Log.i("[QR Code] Using first camera found: $first")
Log.i("[Assistant] [QR Code] Using first camera found: $first")
coreContext.core.videoDevice = first
}
}

View file

@ -77,7 +77,7 @@ class RemoteProvisioningViewModel : ViewModel() {
fun fetchAndApply() {
val url = urlToFetch.value.orEmpty()
coreContext.core.provisioningUri = url
Log.w("[Remote Provisioning] Url set to [$url], restarting Core")
Log.w("[Assistant] [Remote Provisioning] Url set to [$url], restarting Core")
fetchInProgress.value = true
coreContext.core.stop()
coreContext.core.start()

View file

@ -35,6 +35,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.doOnAttach
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
@ -58,6 +59,10 @@ import org.linphone.activities.main.viewmodels.SharedMainViewModel
import org.linphone.activities.navigateToDialer
import org.linphone.compatibility.Compatibility
import org.linphone.contact.ContactsUpdatedListenerStub
import org.linphone.core.AuthInfo
import org.linphone.core.AuthMethod
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.CorePreferences
import org.linphone.core.tools.Log
import org.linphone.databinding.MainActivityBinding
@ -108,6 +113,25 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
private var shouldTabsBeVisibleDependingOnDestination = true
private var shouldTabsBeVisibleDueToOrientationAndKeyboard = true
private val authenticationRequestedEvent: MutableLiveData<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?) {
super.onCreate(savedInstanceState)
@ -143,6 +167,14 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
}
}
authenticationRequestedEvent.observe(
this
) {
it.consume { authInfo ->
showAuthenticationRequestedDialog(authInfo)
}
}
if (coreContext.core.accountList.isEmpty()) {
if (corePreferences.firstStart) {
startActivity(Intent(this, AssistantActivity::class.java))
@ -174,9 +206,11 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
override fun onResume() {
super.onResume()
coreContext.contactsManager.addListener(listener)
coreContext.core.addListener(coreListener)
}
override fun onPause() {
coreContext.core.removeListener(coreListener)
coreContext.contactsManager.removeListener(listener)
super.onPause()
}
@ -205,13 +239,17 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
registerComponentCallbacks(componentCallbacks)
findNavController(R.id.nav_host_fragment).addOnDestinationChangedListener(this)
binding.rootCoordinatorLayout.addKeyboardInsetListener { keyboardVisible ->
binding.rootCoordinatorLayout.setKeyboardInsetListener { keyboardVisible ->
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
Log.i(
"[Main Activity] Keyboard is ${if (keyboardVisible) "visible" else "invisible"}, orientation is ${if (portraitOrientation) "portrait" else "landscape"}"
)
shouldTabsBeVisibleDueToOrientationAndKeyboard = !portraitOrientation || !keyboardVisible
updateTabsFragmentVisibility()
for (listener in keyboardVisibilityListeners) {
listener.onKeyboardVisibilityChanged(keyboardVisible)
}
}
initOverlay()
@ -246,6 +284,14 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
updateTabsFragmentVisibility()
}
fun addKeyboardVisibilityListener(listener: AppUtils.KeyboardVisibilityListener) {
keyboardVisibilityListeners.add(listener)
}
fun removeKeyboardVisibilityListener(listener: AppUtils.KeyboardVisibilityListener) {
keyboardVisibilityListeners.remove(listener)
}
fun hideKeyboard() {
currentFocus?.hideKeyboard()
}
@ -343,7 +389,12 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
// Prevent this intent to be processed again
intent.action = null
intent.data = null
intent.extras?.clear()
val extras = intent.extras
if (extras != null) {
for (key in extras.keySet()) {
intent.removeExtra(key)
}
}
}
private fun handleMainIntent(intent: Intent) {
@ -420,6 +471,8 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
}
}
addressToCall = addressToCall.replace("%40", "@")
val address = coreContext.core.interpretUrl(
addressToCall,
LinphoneUtils.applyInternationalPrefix()
@ -630,4 +683,43 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
dialog.show()
}
private fun showAuthenticationRequestedDialog(
authInfo: AuthInfo
) {
val identity = "${authInfo.username}@${authInfo.domain}"
Log.i("[Main Activity] Showing authentication required dialog for account [$identity]")
val dialogViewModel = DialogViewModel(
getString(R.string.dialog_authentication_required_message, identity),
getString(R.string.dialog_authentication_required_title)
)
dialogViewModel.showPassword = true
dialogViewModel.passwordTitle = getString(
R.string.settings_password_protection_dialog_input_hint
)
val dialog = DialogUtils.getDialog(this, dialogViewModel)
dialogViewModel.showCancelButton {
dialog.dismiss()
}
dialogViewModel.showOkButton(
{
Log.i(
"[Main Activity] Updating password for account [$identity] using auth info [$authInfo]"
)
val newPassword = dialogViewModel.password
authInfo.password = newPassword
coreContext.core.addAuthInfo(authInfo)
coreContext.core.refreshRegisters()
dialog.dismiss()
},
getString(R.string.dialog_authentication_required_change_password_label)
)
dialog.show()
}
}

View file

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

View file

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

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
override fun getLayoutId(): Int {
@ -202,15 +213,6 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
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)
isSecure = chatRoom.currentParams.isEncryptionEnabled
@ -469,7 +471,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
} else {
if (path.isEmpty()) {
val name = content.name
if (name != null && name.isNotEmpty()) {
if (!name.isNullOrEmpty()) {
val file = FileUtils.getFileStoragePath(name)
FileUtils.writeIntoFile(content.buffer, file)
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"
)
} else if (content.isIcalendar) {
val name = "conference.ics"
val file = FileUtils.getFileStoragePath(name)
val filename = "conference.ics"
val file = FileUtils.getFileStoragePath(filename)
FileUtils.writeIntoFile(content.buffer, file)
path = file.absolutePath
content.filePath = path
@ -498,20 +500,20 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
val extension = FileUtils.getExtensionFromFileName(path)
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
when {
FileUtils.isMimeImage(mime) -> navigateToImageFileViewer(
when (FileUtils.getMimeType(mime)) {
FileUtils.MimeType.Image -> navigateToImageFileViewer(
preventScreenshots
)
FileUtils.isMimeVideo(mime) -> navigateToVideoFileViewer(
FileUtils.MimeType.Video -> navigateToVideoFileViewer(
preventScreenshots
)
FileUtils.isMimeAudio(mime) -> navigateToAudioFileViewer(
FileUtils.MimeType.Audio -> navigateToAudioFileViewer(
preventScreenshots
)
FileUtils.isMimePdf(mime) -> navigateToPdfFileViewer(
FileUtils.MimeType.Pdf -> navigateToPdfFileViewer(
preventScreenshots
)
FileUtils.isMimePlainText(mime) -> navigateToTextFileViewer(
FileUtils.MimeType.PlainText -> navigateToTextFileViewer(
preventScreenshots
)
else -> {
@ -584,9 +586,9 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
viewLifecycleOwner
) {
it.consume { chatMessage ->
var index = 0
var retryCount = 0
var expectedChildCount = 0
var index: Int
var loadSteps = 0
var expectedChildCount: Int
do {
val events = listViewModel.events.value.orEmpty()
expectedChildCount = events.size
@ -600,18 +602,17 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
}
index = events.indexOf(eventLog)
if (index == -1) {
retryCount += 1
loadSteps += 1
listViewModel.loadMoreData(events.size)
}
} while (index == -1 && retryCount < 5)
} while (index == -1 && loadSteps < 5)
if (index != -1) {
if (retryCount == 0) {
if (loadSteps == 0) {
scrollTo(index, true)
} else {
lifecycleScope.launch {
withContext(Dispatchers.Default) {
val layoutManager = binding.chatMessagesList.layoutManager as LinearLayoutManager
var retryCount = 0
do {
// 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) {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
@ -846,7 +847,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
if (_adapter != null) {
try {
adapter.registerAdapterDataObserver(observer)
} catch (ise: IllegalStateException) {}
} catch (_: IllegalStateException) {}
}
// 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!"
)
}
(requireActivity() as MainActivity).addKeyboardVisibilityListener(
keyboardVisibilityListener
)
}
override fun onPause() {
@ -871,12 +876,16 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
if (_adapter != null) {
try {
adapter.unregisterAdapterDataObserver(observer)
} catch (ise: IllegalStateException) {}
} catch (_: IllegalStateException) {}
}
// Conversation isn't visible anymore, any new message received in it will trigger a notification
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
(requireActivity() as MainActivity).removeKeyboardVisibilityListener(
keyboardVisibilityListener
)
super.onPause()
}

View file

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

View file

@ -25,6 +25,8 @@ import android.text.method.LinkMovementMethod
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.round
/**
* The purpose of this class is to have a TextView declared with wrap_content as width that won't
@ -52,10 +54,12 @@ class MultiLineWrapContentWidthTextView : AppCompatTextView {
if (layout != null && layout.lineCount >= 2) {
val maxLineWidth = ceil(getMaxLineWidth(layout)).toInt()
val uselessPaddingWidth = layout.width - maxLineWidth
val width = measuredWidth - uselessPaddingWidth
val height = measuredHeight
setMeasuredDimension(width, height)
if (maxLineWidth < measuredWidth) {
super.onMeasure(
MeasureSpec.makeMeasureSpec(maxLineWidth, MeasureSpec.getMode(widthSpec)),
heightSpec
)
}
}
}
@ -63,10 +67,8 @@ class MultiLineWrapContentWidthTextView : AppCompatTextView {
var maxWidth = 0.0f
val lines = layout.lineCount
for (i in 0 until lines) {
if (layout.getLineWidth(i) > maxWidth) {
maxWidth = layout.getLineWidth(i)
}
maxWidth = max(maxWidth, layout.getLineWidth(i))
}
return maxWidth
return round(maxWidth)
}
}

View file

@ -89,13 +89,26 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
address.value = conferenceAddress!!
if (scheduleForLater.value == true && sendInviteViaChat.value == true) {
// Send conference info even when conf is not scheduled for later
// as the conference server doesn't invite participants automatically
val chatRoomParams = LinphoneUtils.getConferenceInvitationsChatRoomParams()
conferenceScheduler.sendInvitations(chatRoomParams)
if (scheduleForLater.value == true) {
if (sendInviteViaChat.value == true) {
// Send conference info even when conf is not scheduled for later
// as the conference server doesn't invite participants automatically
Log.i(
"[Conference Creation] Scheduled conference is ready, sending invitations by chat"
)
val chatRoomParams = LinphoneUtils.getConferenceInvitationsChatRoomParams()
conferenceScheduler.sendInvitations(chatRoomParams)
} else {
Log.i(
"[Conference Creation] Scheduled conference is ready, we were asked not to send invitations by chat so leaving fragment"
)
conferenceCreationInProgress.value = false
conferenceCreationCompletedEvent.value = Event(true)
}
} else {
// Will be done in coreListener
Log.i("[Conference Creation] Group call is ready, leaving fragment")
conferenceCreationInProgress.value = false
conferenceCreationCompletedEvent.value = Event(true)
}
} else if (state == ConferenceScheduler.State.Error) {
Log.e("[Conference Creation] Failed to create conference!")
@ -134,30 +147,6 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
}
}
private val coreListener: CoreListenerStub = object : CoreListenerStub() {
override fun onCallStateChanged(
core: Core,
call: Call,
state: Call.State?,
message: String
) {
when (state) {
Call.State.OutgoingProgress -> {
conferenceCreationInProgress.value = false
}
Call.State.End -> {
Log.i("[Conference Creation] Call has ended, leaving waiting room fragment")
conferenceCreationCompletedEvent.value = Event(true)
}
Call.State.Error -> {
Log.w("[Conference Creation] Call has failed, leaving waiting room fragment")
conferenceCreationCompletedEvent.value = Event(true)
}
else -> {}
}
}
}
init {
sipContactsSelected.value = true
@ -191,11 +180,9 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
}
conferenceScheduler.addListener(listener)
coreContext.core.addListener(coreListener)
}
override fun onCleared() {
coreContext.core.removeListener(coreListener)
conferenceScheduler.removeListener(listener)
participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy)

View file

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

View file

@ -20,6 +20,7 @@
package org.linphone.activities.main.contact.fragments
import android.app.Activity
import android.app.Dialog
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
@ -38,9 +39,11 @@ import org.linphone.activities.main.MainActivity
import org.linphone.activities.main.contact.data.ContactEditorData
import org.linphone.activities.main.contact.data.NumberOrAddressEditorData
import org.linphone.activities.main.contact.viewmodels.*
import org.linphone.activities.main.viewmodels.DialogViewModel
import org.linphone.activities.navigateToContact
import org.linphone.core.tools.Log
import org.linphone.databinding.ContactEditorFragmentBinding
import org.linphone.utils.DialogUtils
import org.linphone.utils.FileUtils
import org.linphone.utils.PermissionHelper
@ -71,10 +74,38 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
data.syncAccountName = null
data.syncAccountType = null
if (data.friend == null && corePreferences.showNewContactAccountDialog) {
Log.i("[Contact Editor] New contact, ask user where to store it")
SyncAccountPickerFragment(this).show(childFragmentManager, "SyncAccountPicker")
if (data.friend == null) {
var atLeastASipAddressOrPhoneNumber = false
for (addr in data.addresses.value.orEmpty()) {
if (addr.newValue.value.orEmpty().isNotEmpty()) {
atLeastASipAddressOrPhoneNumber = true
break
}
}
if (!atLeastASipAddressOrPhoneNumber) {
for (number in data.numbers.value.orEmpty()) {
if (number.newValue.value.orEmpty().isNotEmpty()) {
atLeastASipAddressOrPhoneNumber = true
break
}
}
}
if (!atLeastASipAddressOrPhoneNumber) {
// Contact will be created without phone and SIP address
// Let's warn the user it won't be visible in Linphone app
Log.w(
"[Contact Editor] New contact without SIP address nor phone number, showing warning dialog"
)
showInvisibleContactWarningDialog()
} else if (corePreferences.showNewContactAccountDialog) {
Log.i("[Contact Editor] New contact, ask user where to store it")
SyncAccountPickerFragment(this).show(childFragmentManager, "SyncAccountPicker")
} else {
Log.i("[Contact Editor] Saving new contact")
saveContact()
}
} else {
Log.i("[Contact Editor] Saving contact changes")
saveContact()
}
}
@ -98,7 +129,7 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
}
override fun onSyncAccountClicked(name: String?, type: String?) {
Log.i("[Contact Editor] Using account $name / $type")
Log.i("[Contact Editor] Saving new contact using account $name / $type")
data.syncAccountName = name
data.syncAccountType = type
saveContact()
@ -146,6 +177,9 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
Log.i("[Contact Editor] Displaying contact $savedContact")
navigateToContact(id)
} else {
Log.w(
"[Contact Editor] Can't display $savedContact because it doesn't have a refKey, going back"
)
goBack()
}
}
@ -183,4 +217,35 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
startActivityForResult(chooserIntent, 0)
}
private fun showInvisibleContactWarningDialog() {
val dialogViewModel =
DialogViewModel(getString(R.string.contacts_new_contact_wont_be_visible_warning_dialog))
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
dialogViewModel.showCancelButton(
{
Log.i("[Contact Editor] Aborting new contact saving")
dialog.dismiss()
},
getString(R.string.no)
)
dialogViewModel.showOkButton(
{
dialog.dismiss()
if (corePreferences.showNewContactAccountDialog) {
Log.i("[Contact Editor] New contact, ask user where to store it")
SyncAccountPickerFragment(this).show(childFragmentManager, "SyncAccountPicker")
} else {
Log.i("[Contact Editor] Saving new contact")
saveContact()
}
},
getString(R.string.yes)
)
dialog.show()
}
}

View file

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

View file

@ -119,17 +119,17 @@ class ContactsListViewModel : ViewModel() {
previousFilter = filterValue
val domain = if (sipContactsSelected.value == true) coreContext.core.defaultAccount?.params?.domain ?: "" else ""
val filter = MagicSearch.Source.Friends.toInt() or MagicSearch.Source.LdapServers.toInt()
val sources = MagicSearch.Source.Friends.toInt() or MagicSearch.Source.LdapServers.toInt()
val aggregation = MagicSearch.Aggregation.Friend
searchResultsPending = true
fastFetchJob?.cancel()
Log.i(
"[Contacts] Asking Magic search for contacts matching filter [$filterValue], domain [$domain] and in sources [$filter]"
"[Contacts] Asking Magic search for contacts matching filter [$filterValue], domain [$domain] and in sources [$sources]"
)
coreContext.contactsManager.magicSearch.getContactsListAsync(
filterValue,
domain,
filter,
sources,
aggregation
)

View file

@ -273,6 +273,24 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
// Don't check the following the previous permissions are being asked
checkTelecomManagerPermissions()
}
// See https://developer.android.com/about/versions/14/behavior-changes-14#fgs-types
if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) {
val fullScreenIntentPermission = Compatibility.hasFullScreenIntentPermission(
requireContext()
)
Log.i(
"[Dialer] Android 14 or above detected: full-screen intent permission is ${if (fullScreenIntentPermission) "granted" else "not granted"}"
)
if (!fullScreenIntentPermission) {
(requireActivity() as MainActivity).showSnackBar(
R.string.android_14_full_screen_intent_permission_not_granted,
R.string.android_14_go_to_full_screen_intent_permission_setting
) {
Compatibility.requestFullScreenIntentPermission(requireContext())
}
}
}
}
@TargetApi(Version.API26_O_80)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -98,14 +98,6 @@ class AccountSettingsFragment : GenericSettingFragment<SettingsAccountFragmentBi
}
}
viewModel.publishPresenceToggledEvent.observe(
viewLifecycleOwner
) {
it.consume {
sharedViewModel.publishPresenceToggled.value = true
}
}
viewModel.deleteAccountRequiredEvent.observe(
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() {
override fun onClicked() {
Log.i("[Contacts Settings] Clicked on new LDAP config")

View file

@ -75,10 +75,6 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
MutableLiveData<Event<Boolean>>()
}
val publishPresenceToggledEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val displayUsernameInsteadOfIdentity = corePreferences.replaceSipUriByUsername
private var accountToDelete: Account? = null
@ -293,7 +289,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
account.params = params
transportIndex.value = account.params.transport.toInt()
} else {
Log.e("[Account Settings] Couldn't parse address: $address")
Log.e("[Account Settings] Couldn't parse address: $newValue")
}
}
}
@ -439,16 +435,6 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
}
val limeServerUrl = MutableLiveData<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() {
override fun onBoolValueChanged(newValue: Boolean) {
val params = account.params.clone()
@ -518,7 +504,6 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
limeServerUrl.value = params.limeServerUrl
hideLinkPhoneNumber.value = corePreferences.hideLinkPhoneNumber || params.identityAddress?.domain != corePreferences.defaultDomain
publishPresence.value = params.isPublishEnabled
disableBundleMode.value = !params.isRtpBundleEnabled
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -87,7 +87,17 @@ class VideoSettingsViewModel : GenericSettingsViewModel() {
val videoPresetListener = object : SettingListenerStub() {
override fun onListValueChanged(position: Int) {
videoPresetIndex.value = position // Needed to display/hide two below settings
core.videoPreset = videoPresetLabels.value.orEmpty()[position]
val currentPreset = core.videoPreset
val newPreset = videoPresetLabels.value.orEmpty()[position]
if (newPreset != currentPreset) {
if (currentPreset == "custom") {
// Not "custom" anymore, reset FPS & bandwidth
core.preferredFramerate = 0f
core.downloadBandwidth = 0
core.uploadBandwidth = 0
}
core.videoPreset = newPreset
}
}
}
val videoPresetIndex = MutableLiveData<Int>()
@ -106,7 +116,7 @@ class VideoSettingsViewModel : GenericSettingsViewModel() {
try {
core.downloadBandwidth = newValue.toInt()
core.uploadBandwidth = newValue.toInt()
} catch (nfe: NumberFormatException) {
} catch (_: NumberFormatException) {
}
}
}

View file

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

View file

@ -62,8 +62,6 @@ class CallsListFragment : GenericVideoPreviewFragment<VoipCallsListFragmentBindi
binding.controlsViewModel = controlsViewModel
setupLocalViewPreview(binding.localPreviewVideoSurface, binding.switchCamera)
binding.setCancelClickListener {
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) {
val popupView: VoipCallContextMenuBindingImpl = DataBindingUtil.inflate(
LayoutInflater.from(requireContext()),

View file

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

View file

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

View file

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

View file

@ -24,7 +24,6 @@ import android.os.SystemClock
import android.view.View
import android.widget.Chronometer
import androidx.navigation.navGraphViewModels
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.activities.navigateToActiveCall
import org.linphone.activities.voip.viewmodels.CallsViewModel
@ -46,8 +45,6 @@ class OutgoingCallFragment : GenericVideoPreviewFragment<VoipCallOutgoingFragmen
binding.callsViewModel = callsViewModel
setupLocalViewPreview(binding.localPreviewVideoSurface, binding.switchCamera)
callsViewModel.callConnectedEvent.observe(
viewLifecycleOwner
) {
@ -79,7 +76,7 @@ class OutgoingCallFragment : GenericVideoPreviewFragment<VoipCallOutgoingFragmen
viewLifecycleOwner
) {
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
override fun onResume() {
super.onResume()
controlsViewModel.forceDisableProximitySensor.value = true
if (controlsViewModel.isOutgoingEarlyMedia.value == true) {
setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera)
}
}
override fun onPause() {
controlsViewModel.forceDisableProximitySensor.value = false
super.onPause()
controlsViewModel.forceDisableProximitySensor.value = false
cleanUpLocalVideoPreview(binding.localPreviewVideoSurface)
}
}

View file

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

View file

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

View file

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

View file

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

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

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.PendingIntent
import android.app.Service
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
@ -290,7 +291,7 @@ class Compatibility {
}
}
fun startForegroundService(service: Service, notifId: Int, notif: Notification?) {
private fun startForegroundService(service: Service, notifId: Int, notif: Notification?) {
if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
Api31Compatibility.startForegroundService(service, notifId, notif)
} else {
@ -298,6 +299,22 @@ class Compatibility {
}
}
fun startCallForegroundService(service: Service, notifId: Int, notif: Notification) {
if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) {
Api34Compatibility.startCallForegroundService(service, notifId, notif)
} else {
startForegroundService(service, notifId, notif)
}
}
fun startDataSyncForegroundService(service: Service, notifId: Int, notif: Notification) {
if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) {
Api34Compatibility.startDataSyncForegroundService(service, notifId, notif)
} else {
startForegroundService(service, notifId, notif)
}
}
/* Call */
fun canDrawOverlay(context: Context): Boolean {
@ -455,5 +472,26 @@ class Compatibility {
}
return false
}
fun clearClipboard(clipboard: ClipboardManager) {
if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) {
Api28Compatibility.clearClipboard(clipboard)
}
}
fun hasFullScreenIntentPermission(context: Context): Boolean {
if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) {
return Api34Compatibility.hasFullScreenIntentPermission(context)
}
return true
}
fun requestFullScreenIntentPermission(context: Context): Boolean {
if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) {
Api34Compatibility.requestFullScreenIntentPermission(context)
return true
}
return false
}
}
}

View file

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

View file

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

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?) {
Log.i("[Context] Push notification received: $payload")
}
@ -431,18 +425,22 @@ class CoreContext(
}
fun onForeground() {
// If presence publish is disabled and we call core.setConsolidatedPresence, it will enabled it!
if (core.defaultAccount?.params?.isPublishEnabled == true) {
Log.i("[Context] App is in foreground, setting consolidated presence to Online")
// We can't rely on defaultAccount?.params?.isPublishEnabled
// as it will be modified by the SDK when changing the presence status
if (corePreferences.publishPresence) {
Log.i("[Context] App is in foreground, PUBLISHING presence as Online")
core.consolidatedPresence = ConsolidatedPresence.Online
}
}
fun onBackground() {
// If presence publish is disabled and we call core.setConsolidatedPresence, it will enabled it!
if (core.defaultAccount?.params?.isPublishEnabled == true) {
Log.i("[Context] App is in background, setting consolidated presence to Busy")
core.consolidatedPresence = ConsolidatedPresence.Busy
// We can't rely on defaultAccount?.params?.isPublishEnabled
// as it will be modified by the SDK when changing the presence status
if (corePreferences.publishPresence) {
Log.i("[Context] App is in background, un-PUBLISHING presence info")
// We don't use ConsolidatedPresence.Busy but Offline to do an unsubscribe,
// Flexisip will handle the Busy status depending on other devices
core.consolidatedPresence = ConsolidatedPresence.Offline
}
}
@ -1039,8 +1037,8 @@ class CoreContext(
val extension = FileUtils.getExtensionFromFileName(filePath)
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
when {
FileUtils.isMimeImage(mime) -> {
when (FileUtils.getMimeType(mime)) {
FileUtils.MimeType.Image -> {
if (Compatibility.addImageToMediaStore(context, content)) {
Log.i(
"[Context] Successfully exported image [${content.name}] to Media Store"
@ -1051,7 +1049,7 @@ class CoreContext(
)
}
}
FileUtils.isMimeVideo(mime) -> {
FileUtils.MimeType.Video -> {
if (Compatibility.addVideoToMediaStore(context, content)) {
Log.i(
"[Context] Successfully exported video [${content.name}] to Media Store"
@ -1062,7 +1060,7 @@ class CoreContext(
)
}
}
FileUtils.isMimeAudio(mime) -> {
FileUtils.MimeType.Audio -> {
if (Compatibility.addAudioToMediaStore(context, content)) {
Log.i(
"[Context] Successfully exported audio [${content.name}] to Media Store"

View file

@ -53,6 +53,7 @@ class CorePreferences constructor(private val context: Context) {
context,
MasterKey.DEFAULT_MASTER_KEY_ALIAS
).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
try {
EncryptedSharedPreferences.create(
context,
@ -280,6 +281,12 @@ class CorePreferences constructor(private val context: Context) {
config.setBool("app", "contact_shortcuts", value)
}
var publishPresence: Boolean
get() = config.getBool("app", "publish_presence", true)
set(value) {
config.setBool("app", "publish_presence", value)
}
/* Call */
var sendEarlyMedia: Boolean

View file

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

View file

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

View file

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

View file

@ -73,7 +73,7 @@ fun View.hideKeyboard() {
} catch (_: Exception) {}
}
fun View.addKeyboardInsetListener(lambda: (visible: Boolean) -> Unit) {
fun View.setKeyboardInsetListener(lambda: (visible: Boolean) -> Unit) {
doOnLayout {
var isKeyboardVisible = ViewCompat.getRootWindowInsets(this)?.isVisible(
WindowInsetsCompat.Type.ime()
@ -81,8 +81,9 @@ fun View.addKeyboardInsetListener(lambda: (visible: Boolean) -> Unit) {
lambda(isKeyboardVisible)
// See https://issuetracker.google.com/issues/281942480
ViewCompat.setOnApplyWindowInsetsListener(
this
rootView
) { view, insets ->
val keyboardVisibilityChanged = ViewCompat.getRootWindowInsets(view)
?.isVisible(WindowInsetsCompat.Type.ime()) == true
@ -90,7 +91,7 @@ fun View.addKeyboardInsetListener(lambda: (visible: Boolean) -> Unit) {
isKeyboardVisible = keyboardVisibilityChanged
lambda(isKeyboardVisible)
}
insets
ViewCompat.onApplyWindowInsets(view, insets)
}
}
}
@ -343,7 +344,7 @@ fun setImageViewScaleType(imageView: ImageView, scaleType: ImageView.ScaleType)
@BindingAdapter("coilRounded")
fun loadRoundImageWithCoil(imageView: ImageView, path: String?) {
if (path != null && path.isNotEmpty() && FileUtils.isExtensionImage(path)) {
if (!path.isNullOrEmpty() && FileUtils.isExtensionImage(path)) {
imageView.load(path) {
transformations(CircleCropTransformation())
}
@ -354,7 +355,7 @@ fun loadRoundImageWithCoil(imageView: ImageView, path: String?) {
@BindingAdapter("coil")
fun loadImageWithCoil(imageView: ImageView, path: String?) {
if (path != null && path.isNotEmpty() && FileUtils.isExtensionImage(path)) {
if (!path.isNullOrEmpty() && FileUtils.isExtensionImage(path)) {
if (corePreferences.vfsEnabled && path.startsWith(corePreferences.vfsCachePath)) {
imageView.load(path) {
diskCachePolicy(CachePolicy.DISABLED)
@ -551,9 +552,23 @@ fun loadAvatarWithCoil(imageView: ImageView, path: String?) {
@BindingAdapter("coilVideoPreview")
fun loadVideoPreview(imageView: ImageView, path: String?) {
if (path != null && path.isNotEmpty() && FileUtils.isExtensionVideo(path)) {
if (!path.isNullOrEmpty() && FileUtils.isExtensionVideo(path)) {
imageView.load(path) {
videoFrameMillis(0)
listener(
onError = { _, result ->
Log.e(
"[Data Binding] [Coil] Error getting preview picture from video? [$path]: ${result.throwable}"
)
},
onSuccess = { _, _ ->
// Display "play" button above video preview
LayoutInflater.from(imageView.context).inflate(
R.layout.video_play_button,
imageView.parent as ViewGroup
)
}
)
}
}
}
@ -582,13 +597,23 @@ fun addPhoneNumberEditTextValidation(editText: EditText, enabled: Boolean) {
fun addPrefixEditTextValidation(editText: EditText, enabled: Boolean) {
if (!enabled) return
editText.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {}
override fun afterTextChanged(s: Editable?) {
val dialPlan = PhoneNumberUtils.getDialPlanFromCountryCallingPrefix(
s.toString().substring(1)
)
if (dialPlan == null) {
editText.error =
editText.context.getString(
R.string.assistant_error_invalid_international_prefix
)
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
@SuppressLint("SetTextI18n")
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (s == null || s.isEmpty() || !s.startsWith("+")) {
if (s.isNullOrEmpty() || !s.startsWith("+")) {
editText.setText("+$s")
}
}

View file

@ -26,7 +26,10 @@ import android.content.Intent
import android.database.CursorIndexOutOfBoundsException
import android.net.Uri
import android.os.Environment
import android.os.ParcelFileDescriptor
import android.os.Process.myUid
import android.provider.OpenableColumns
import android.system.Os.fstat
import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider
import java.io.*
@ -42,6 +45,15 @@ import org.linphone.R
import org.linphone.core.tools.Log
class FileUtils {
enum class MimeType {
PlainText,
Pdf,
Image,
Video,
Audio,
Unknown
}
companion object {
fun getNameFromFilePath(filePath: String): String {
var name = filePath
@ -64,36 +76,28 @@ class FileUtils {
return extension.lowercase(Locale.getDefault())
}
fun isMimePlainText(type: String?): Boolean {
return type?.startsWith("text/plain") ?: false
}
fun isMimePdf(type: String?): Boolean {
return type?.startsWith("application/pdf") ?: false
}
fun isMimeImage(type: String?): Boolean {
return type?.startsWith("image/") ?: false
}
fun isMimeVideo(type: String?): Boolean {
return type?.startsWith("video/") ?: false
}
fun isMimeAudio(type: String?): Boolean {
return type?.startsWith("audio/") ?: false
fun getMimeType(type: String?): MimeType {
if (type.isNullOrEmpty()) return MimeType.Unknown
return when {
type.startsWith("image/") -> MimeType.Image
type.startsWith("text/plain") -> MimeType.PlainText
type.startsWith("video/") -> MimeType.Video
type.startsWith("audio/") -> MimeType.Audio
type.startsWith("application/pdf") -> MimeType.Pdf
else -> MimeType.Unknown
}
}
fun isExtensionImage(path: String): Boolean {
val extension = getExtensionFromFileName(path)
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
return isMimeImage(type)
return getMimeType(type) == MimeType.Image
}
fun isExtensionVideo(path: String): Boolean {
val extension = getExtensionFromFileName(path)
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
return isMimeVideo(type)
return getMimeType(type) == MimeType.Video
}
fun clearExistingPlainFiles() {
@ -269,6 +273,21 @@ class FileUtils {
var result: String? = null
val name: String = getNameFromUri(uri, context)
try {
if (fstat(
ParcelFileDescriptor.open(
File(uri.path),
ParcelFileDescriptor.MODE_READ_ONLY
).fileDescriptor
).st_uid != myUid()
) {
Log.e("[File Utils] File descriptor UID different from our, denying copy!")
return result
}
} catch (e: Exception) {
Log.e("[File Utils] Can't check file ownership: ", e)
}
try {
val localFile: File = createFile(name)
val remoteFile =

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,7 @@
<?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>
<import type="android.view.View"/>
@ -14,8 +16,15 @@
android:layout_margin="5dp"
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
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:layout_width="100dp"
android:layout_height="100dp"
@ -25,8 +34,25 @@
android:scaleType="centerCrop"
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
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:layout_width="100dp"
android:layout_height="100dp"
@ -36,41 +62,23 @@
android:scaleType="centerCrop"
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"
<TextView
style="@style/chat_file_attachment_font"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:orientation="vertical"
android:layout_marginBottom="5dp"
android:layout_marginRight="10dp"
android:ellipsize="middle"
android:singleLine="true"
android:padding="10dp"
android:gravity="center"
android:background="?attr/backgroundColor"
android:visibility="@{data.image || data.video ? View.GONE : View.VISIBLE}">
<TextView
style="@style/chat_file_attachment_font"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:ellipsize="middle"
android:singleLine="true"
android:padding="10dp"
android:gravity="center"
android:textAlignment="center"
android:drawablePadding="5dp"
android:drawableTop="@{data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : @drawable/file), default=@drawable/file}"
android:text="@{data.fileName, default=`test.txt`}"/>
</LinearLayout>
android:textAlignment="center"
android:drawablePadding="5dp"
android:visibility="@{data.image || data.video ? View.GONE : View.VISIBLE, default=gone}"
android:text="@{data.fileName, default=`test.txt`}"
android:drawableTop="@{data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : @drawable/file), default=@drawable/file}"
tools:ignore="UseCompatTextViewDrawableXml" />
<ImageView
android:onClick="@{() -> data.delete()}"

View file

@ -1,5 +1,7 @@
<?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>
<import type="android.view.View" />
@ -29,7 +31,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:drawableTop="@{data.video ? @drawable/file_video : (data.image ? @drawable/file_picture : (data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : (data.voiceRecording ? @drawable/audio_recording_reply_preview_default : @drawable/file)))), default=@drawable/file}"
android:drawablePadding="5dp"
android:ellipsize="middle"
android:gravity="center"
@ -37,7 +38,9 @@
android:onLongClick="@{longClickListener}"
android:singleLine="true"
android:text="@{data.fileName, default=`test.pdf`}"
android:textAlignment="center" />
android:textAlignment="center"
android:drawableTop="@{data.video ? @drawable/file_video : (data.image ? @drawable/file_picture : (data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : (data.voiceRecording ? @drawable/audio_recording_reply_preview_default : @drawable/file)))), default=@drawable/file}"
tools:ignore="UseCompatTextViewDrawableXml" />
<TextView
style="@style/chat_file_attachment_font"

View file

@ -1,5 +1,7 @@
<?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>
<import type="android.view.View" />
@ -15,6 +17,8 @@
</data>
<RelativeLayout
android:onClick="@{() -> data.openFile()}"
android:onLongClick="@{longClickListener}"
android:layout_width="@dimen/chat_message_bubble_file_size"
android:layout_height="wrap_content"
android:minHeight="@dimen/chat_message_bubble_file_size"
@ -29,15 +33,14 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:drawableTop="@{data.video ? @drawable/file_video : (data.image ? @drawable/file_picture : (data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : (data.voiceRecording ? @drawable/audio_recording_reply_preview_default : @drawable/file)))), default=@drawable/file}"
android:drawablePadding="5dp"
android:ellipsize="middle"
android:gravity="center"
android:onClick="@{() -> data.openFile()}"
android:onLongClick="@{longClickListener}"
android:singleLine="true"
android:text="@{data.fileName, default=`test.pdf`}"
android:textAlignment="center" />
android:textAlignment="center"
android:drawableTop="@{data.video ? @drawable/file_video : (data.image ? @drawable/file_picture : (data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : (data.voiceRecording ? @drawable/audio_recording_reply_preview_default : @drawable/file)))), default=@drawable/file}"
tools:ignore="UseCompatTextViewDrawableXml" />
</RelativeLayout>

View file

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

View file

@ -85,7 +85,8 @@
android:background="@drawable/led_background"
android:padding="2dp"
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
android:id="@+id/background"

View file

@ -26,6 +26,15 @@
android:scaleType="@{ScaleType.CENTER_CROP}"
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
android:contentDescription="@string/content_description_downloaded_file_transfer"
android:layout_width="wrap_content"
@ -37,16 +46,6 @@
android:scaleType="@{ScaleType.CENTER_CROP}"
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
android:visibility="@{data.isVoiceRecording ? View.VISIBLE : View.GONE, default=gone}"
android:layout_width="wrap_content"

View file

@ -24,6 +24,16 @@
android:scaleType="@{ScaleType.CENTER_CROP}"
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
android:contentDescription="@string/content_description_downloaded_file_transfer"
android:layout_width="@dimen/chat_message_small_bubble_file_size"
@ -34,17 +44,6 @@
android:scaleType="@{ScaleType.CENTER_CROP}"
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
android:visibility="@{data.isVoiceRecording ? View.VISIBLE : View.GONE, default=gone}"
android:layout_width="wrap_content"
@ -74,10 +73,7 @@
<ImageView
android:layout_width="@dimen/chat_message_small_bubble_file_size"
android:layout_height="@dimen/chat_message_small_bubble_file_size"
android:layout_marginStart="5dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="5dp"
android:layout_marginBottom="5dp"
android:layout_margin="5dp"
android:background="@drawable/chat_bubble_reply_file_background"
android:contentDescription="@{data.fileName}"
android:padding="10dp"

View file

@ -1,5 +1,6 @@
<?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>
<import type="android.view.View" />
@ -16,14 +17,29 @@
</data>
<RelativeLayout
android:onClick="@{() -> data.openFile()}"
android:onLongClick="@{longClickListener}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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
android:onClick="@{() -> data.openFile()}"
android:onLongClick="@{longClickListener}"
android:contentDescription="@string/content_description_downloaded_file_transfer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -33,15 +49,6 @@
android:scaleType="@{data.alone ? ScaleType.FIT_CENTER : ScaleType.CENTER_CROP}"
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>
</layout>

View file

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

View file

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

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
<import type="android.view.View"/>
@ -49,7 +50,8 @@
android:background="@drawable/led_background"
android:padding="@dimen/contact_presence_badge_padding"
app:presenceIcon="@{data.presenceStatus}"
android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" />
android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}"
tools:ignore="ContentDescription" />
<ImageView
android:layout_width="20dp"

View file

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

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
<import type="android.view.View"/>
@ -37,7 +38,8 @@
android:background="@drawable/led_background"
android:padding="@dimen/contact_presence_badge_padding"
app:presenceIcon="@{data.presenceStatus}"
android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" />
android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/time"

View file

@ -62,7 +62,8 @@
app:presenceIcon="@{data.presenceStatus}"
android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}"
app:layout_constraintStart_toStartOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"/>
app:layout_constraintBottom_toBottomOf="@id/avatar"
tools:ignore="ContentDescription"/>
<ImageView
android:id="@+id/securityLevel"

View file

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

View file

@ -125,9 +125,8 @@
android:layout_width="@dimen/chat_message_sending_icons_size"
android:layout_height="0dp"
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:enabled="@{chatSendingViewModel.attachFileEnabled &amp;&amp; !chatSendingViewModel.attachFilePending}"
android:enabled="@{chatSendingViewModel.attachFileEnabled &amp;&amp; !chatSendingViewModel.attachFilePending &amp;&amp; chatSendingViewModel.isFileTransferAvailable}"
android:onClick="@{attachFileClickListener}"
android:paddingTop="@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_height="0dp"
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:onClick="@{() -> chatSendingViewModel.toggleVoiceRecording()}"
android:onTouch="@{voiceRecordingTouchListener}"
android:paddingTop="@dimen/chat_message_sending_icons_margin"
android:paddingBottom="@dimen/chat_message_sending_icons_margin"
android:selected="@{chatSendingViewModel.isVoiceRecording}"
android:enabled="@{chatSendingViewModel.isFileTransferAvailable}"
android:src="@drawable/record_audio_message"
app:layout_constraintHeight_max="@dimen/chat_message_sending_icons_size"
app:layout_constraintBottom_toBottomOf="@id/message"
@ -162,6 +161,7 @@
android:layout_below="@id/emoji_picker"
android:layout_gravity="center_vertical"
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_marginBottom="10dp"
android:background="@color/header_background_color"
@ -174,37 +174,21 @@
android:textColor="@color/black_color"
android:textCursorDrawable="@null"
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_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
android:id="@+id/emoji_picker_toggle"
android:layout_width="@dimen/chat_message_sending_icons_size"
android:layout_height="0dp"
android:layout_marginStart="@dimen/chat_message_sending_icons_margin"
android:layout_marginEnd="@dimen/chat_message_sending_icons_margin"
android:onClick="@{() -> chatSendingViewModel.toggleEmojiPicker()}"
android:paddingTop="@dimen/chat_message_sending_icons_margin"
android:paddingBottom="@dimen/chat_message_sending_icons_margin"
android:src="@drawable/emoji"
android:background="@color/header_background_color"
android:contentDescription="@string/content_description_emoji_picker"
android:selected="@{chatSendingViewModel.isEmojiPickerOpen}"
android:visibility="@{chatSendingViewModel.isEmojiPickerVisible ? View.VISIBLE : View.GONE}"
app:layout_constraintHeight_max="@dimen/chat_message_sending_icons_size"
@ -216,7 +200,6 @@
android:id="@+id/send_message"
android:layout_width="@dimen/chat_message_sending_icons_size"
android:layout_height="0dp"
android:layout_marginStart="@dimen/chat_message_sending_icons_margin"
android:layout_marginEnd="@dimen/chat_message_sending_icons_margin"
android:contentDescription="@string/content_description_send_message"
android:enabled="@{chatSendingViewModel.sendMessageEnabled &amp;&amp; !chatSendingViewModel.attachingFileInProgress}"

View file

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

View file

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

View file

@ -105,7 +105,8 @@
android:background="@drawable/led_background"
android:padding="@dimen/contact_presence_big_badge_padding"
app:presenceIcon="@{viewModel.presenceStatus}"
android:visibility="@{viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" />
android:visibility="@{viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}"
tools:ignore="ContentDescription" />
</RelativeLayout>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
<import type="android.view.View"/>
@ -54,7 +55,8 @@
android:background="@drawable/led_background"
android:padding="@dimen/contact_presence_badge_padding"
app:presenceIcon="@{viewModel.presenceStatus}"
android:visibility="@{viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" />
android:visibility="@{viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}"
tools:ignore="ContentDescription" />
<LinearLayout
android:id="@+id/right"
@ -72,7 +74,7 @@
android:src="@drawable/linphone_logo_tinted"
android:layout_marginRight="10dp"
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
android:onClick="@{() -> selectionListViewModel.onToggleSelect(position)}"

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