Merge branch 'release/5.1'
This commit is contained in:
commit
ede66c2fa7
133 changed files with 2310 additions and 611 deletions
41
CHANGELOG.md
41
CHANGELOG.md
|
@ -15,17 +15,19 @@ Group changes to describe their impact on the project, as follows:
|
|||
### Added
|
||||
- Chat messages emoji "reactions"
|
||||
|
||||
## [5.1.0] - Unreleased
|
||||
## [5.1.0] - 2023-08-21
|
||||
|
||||
### Added
|
||||
- Showing short term presence for contacts whom publish it + added setting to disable it (enabled by default for sip.linphone.org accounts)
|
||||
- Confirmation dialog before removing account
|
||||
- Attended transfer instead of blind transfer if there is more than 1 call
|
||||
- Added emoji picker in chat room, and increase size of text if it only contains emojis
|
||||
- Added hidden setting to disable video completely
|
||||
- Added hidden setting to prevent adding / editing / removing native contacts
|
||||
- Added hidden setting to protect settings access using account password
|
||||
- Last sent message delivery status (IMDN) icon in chat rooms list
|
||||
- Emoji picker in chat room, and increase size of text if it only contains emojis
|
||||
- Hidden setting to disable video completely
|
||||
- Hidden setting to prevent adding / editing / removing native contacts
|
||||
- Hidden setting to protect settings access using account password
|
||||
- SIP URI in call can be selected using long press
|
||||
- Dialog showing up asking for correct account password in case of failed authentication
|
||||
|
||||
### Changed
|
||||
- Switched Account Creator backend from XMLRPC to FlexiAPI, it now requires to be able to receive a push notification
|
||||
|
@ -38,6 +40,35 @@ Group changes to describe their impact on the project, as follows:
|
|||
|
||||
### Fixed
|
||||
- Messages not marked as reply in basic chat room if sending more than 1 content
|
||||
- Chat message video attachment display when failing to get a preview picture
|
||||
|
||||
## [5.0.14] - 2023-06-20
|
||||
|
||||
### Changed
|
||||
- SDK update only
|
||||
|
||||
## [5.0.13] - 2023-06-15
|
||||
|
||||
### Changed
|
||||
- SDK update only
|
||||
|
||||
## [5.0.12] - 2023-05-23
|
||||
|
||||
### Fixed
|
||||
- Crash if notification manager throws an exception
|
||||
- Video preview not moving if call was started in audio only
|
||||
|
||||
## [5.0.11] - 2023-05-09
|
||||
|
||||
### Fixed
|
||||
- Wrong call displayed when hanging up a call while an incoming one is ringing
|
||||
- Crash related to call history
|
||||
- Crash due to wrongly format string
|
||||
- Add/remove missing listener on FriendLists created after Core has been created
|
||||
|
||||
### Changed
|
||||
- Improved GSM call interruption
|
||||
- Updated translations
|
||||
|
||||
## [5.0.11] - 2023-05-09
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -75,6 +75,10 @@ class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragment
|
|||
startActivity(intent)
|
||||
}
|
||||
|
||||
viewModel.prefix.observe(viewLifecycleOwner) { internationalPrefix ->
|
||||
viewModel.getCountryNameFromPrefix(internationalPrefix)
|
||||
}
|
||||
|
||||
viewModel.goToSmsValidationEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -62,6 +62,10 @@ class PhoneAccountCreationFragment :
|
|||
countryPickerFragment.show(childFragmentManager, "CountryPicker")
|
||||
}
|
||||
|
||||
viewModel.prefix.observe(viewLifecycleOwner) { internationalPrefix ->
|
||||
viewModel.getCountryNameFromPrefix(internationalPrefix)
|
||||
}
|
||||
|
||||
viewModel.goToSmsValidationEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
|
|
|
@ -73,6 +73,10 @@ class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountL
|
|||
countryPickerFragment.show(childFragmentManager, "CountryPicker")
|
||||
}
|
||||
|
||||
viewModel.prefix.observe(viewLifecycleOwner) { internationalPrefix ->
|
||||
viewModel.getCountryNameFromPrefix(internationalPrefix)
|
||||
}
|
||||
|
||||
viewModel.goToSmsValidationEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -98,14 +98,6 @@ class AccountSettingsFragment : GenericSettingFragment<SettingsAccountFragmentBi
|
|||
}
|
||||
}
|
||||
|
||||
viewModel.publishPresenceToggledEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
sharedViewModel.publishPresenceToggled.value = true
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.deleteAccountRequiredEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ class ChatSettingsViewModel : GenericSettingsViewModel() {
|
|||
val maxSize = newValue.toInt()
|
||||
core.maxSizeForAutoDownloadIncomingFiles = maxSize
|
||||
updateAutoDownloadIndexFromMaxSize(maxSize)
|
||||
} catch (nfe: NumberFormatException) {
|
||||
} catch (_: NumberFormatException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -52,7 +52,7 @@ class NetworkSettingsViewModel : GenericSettingsViewModel() {
|
|||
try {
|
||||
val port = newValue.toInt()
|
||||
setTransportPort(port)
|
||||
} catch (nfe: NumberFormatException) {
|
||||
} catch (_: NumberFormatException) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()}"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/backgroundColor"
|
||||
android:clickable="true">
|
||||
|
||||
<LinearLayout
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 && !chatSendingViewModel.attachFilePending}"
|
||||
android:enabled="@{chatSendingViewModel.attachFileEnabled && !chatSendingViewModel.attachFilePending && 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 && !chatSendingViewModel.attachingFileInProgress}"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue