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
|
### Added
|
||||||
- Chat messages emoji "reactions"
|
- Chat messages emoji "reactions"
|
||||||
|
|
||||||
## [5.1.0] - Unreleased
|
## [5.1.0] - 2023-08-21
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Showing short term presence for contacts whom publish it + added setting to disable it (enabled by default for sip.linphone.org accounts)
|
- Showing short term presence for contacts whom publish it + added setting to disable it (enabled by default for sip.linphone.org accounts)
|
||||||
- Confirmation dialog before removing account
|
- Confirmation dialog before removing account
|
||||||
- Attended transfer instead of blind transfer if there is more than 1 call
|
- Attended transfer instead of blind transfer if there is more than 1 call
|
||||||
- Added emoji picker in chat room, and increase size of text if it only contains emojis
|
- Last sent message delivery status (IMDN) icon in chat rooms list
|
||||||
- Added hidden setting to disable video completely
|
- Emoji picker in chat room, and increase size of text if it only contains emojis
|
||||||
- Added hidden setting to prevent adding / editing / removing native contacts
|
- Hidden setting to disable video completely
|
||||||
- Added hidden setting to protect settings access using account password
|
- Hidden setting to prevent adding / editing / removing native contacts
|
||||||
|
- Hidden setting to protect settings access using account password
|
||||||
- SIP URI in call can be selected using long press
|
- SIP URI in call can be selected using long press
|
||||||
|
- Dialog showing up asking for correct account password in case of failed authentication
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Switched Account Creator backend from XMLRPC to FlexiAPI, it now requires to be able to receive a push notification
|
- Switched Account Creator backend from XMLRPC to FlexiAPI, it now requires to be able to receive a push notification
|
||||||
|
@ -38,6 +40,35 @@ Group changes to describe their impact on the project, as follows:
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Messages not marked as reply in basic chat room if sending more than 1 content
|
- Messages not marked as reply in basic chat room if sending more than 1 content
|
||||||
|
- Chat message video attachment display when failing to get a preview picture
|
||||||
|
|
||||||
|
## [5.0.14] - 2023-06-20
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- SDK update only
|
||||||
|
|
||||||
|
## [5.0.13] - 2023-06-15
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- SDK update only
|
||||||
|
|
||||||
|
## [5.0.12] - 2023-05-23
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Crash if notification manager throws an exception
|
||||||
|
- Video preview not moving if call was started in audio only
|
||||||
|
|
||||||
|
## [5.0.11] - 2023-05-09
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Wrong call displayed when hanging up a call while an incoming one is ringing
|
||||||
|
- Crash related to call history
|
||||||
|
- Crash due to wrongly format string
|
||||||
|
- Add/remove missing listener on FriendLists created after Core has been created
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Improved GSM call interruption
|
||||||
|
- Updated translations
|
||||||
|
|
||||||
## [5.0.11] - 2023-05-09
|
## [5.0.11] - 2023-05-09
|
||||||
|
|
||||||
|
|
|
@ -7,16 +7,16 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
def appVersionName = "5.1.0"
|
def appVersionName = "5.1.0"
|
||||||
def appVersionCode = 50090
|
def appVersionCode = 51000
|
||||||
|
|
||||||
static def getPackageName() {
|
def packageName = "org.linphone"
|
||||||
return "org.linphone"
|
|
||||||
}
|
|
||||||
|
|
||||||
def firebaseAvailable = new File(projectDir.absolutePath +'/google-services.json').exists()
|
def firebaseAvailable = new File(projectDir.absolutePath +'/google-services.json').exists()
|
||||||
|
|
||||||
def crashlyticsAvailable = new File(projectDir.absolutePath +'/google-services.json').exists() && new File(LinphoneSdkBuildDir + '/libs/').exists() && new File(LinphoneSdkBuildDir + '/libs-debug/').exists()
|
def crashlyticsAvailable = new File(projectDir.absolutePath +'/google-services.json').exists() && new File(LinphoneSdkBuildDir + '/libs/').exists() && new File(LinphoneSdkBuildDir + '/libs-debug/').exists()
|
||||||
|
|
||||||
|
def extractNativeLibs = false
|
||||||
|
|
||||||
if (firebaseAvailable) {
|
if (firebaseAvailable) {
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply plugin: 'com.google.gms.google-services'
|
||||||
}
|
}
|
||||||
|
@ -83,13 +83,13 @@ android {
|
||||||
targetCompatibility = 17
|
targetCompatibility = 17
|
||||||
}
|
}
|
||||||
|
|
||||||
compileSdkVersion 33
|
compileSdkVersion 34
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 33
|
targetSdkVersion 34
|
||||||
versionCode appVersionCode
|
versionCode appVersionCode
|
||||||
versionName "${project.version}"
|
versionName "${project.version}"
|
||||||
applicationId getPackageName()
|
applicationId packageName
|
||||||
}
|
}
|
||||||
|
|
||||||
applicationVariants.all { variant ->
|
applicationVariants.all { variant ->
|
||||||
|
@ -101,19 +101,19 @@ android {
|
||||||
if (firebaseAvailable) {
|
if (firebaseAvailable) {
|
||||||
enableFirebaseService = "true"
|
enableFirebaseService = "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
// See https://developer.android.com/studio/releases/gradle-plugin#3-6-0-behavior for why extractNativeLibs is set to true in debug flavor
|
// See https://developer.android.com/studio/releases/gradle-plugin#3-6-0-behavior for why extractNativeLibs is set to true in debug flavor
|
||||||
if (variant.buildType.name == "release" || variant.buildType.name == "releaseWithCrashlytics") {
|
if (variant.buildType.name == "release" || variant.buildType.name == "releaseWithCrashlytics") {
|
||||||
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
|
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + packageName + ".provider.sip_address",
|
||||||
linphone_file_provider: getPackageName() + ".fileprovider",
|
linphone_file_provider: packageName + ".fileprovider",
|
||||||
appLabel: "@string/app_name",
|
appLabel: "@string/app_name",
|
||||||
firebaseServiceEnabled: enableFirebaseService,
|
firebaseServiceEnabled: enableFirebaseService]
|
||||||
extractNativeLibs: "false"]
|
|
||||||
} else {
|
} else {
|
||||||
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
|
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + packageName + ".provider.sip_address",
|
||||||
linphone_file_provider: getPackageName() + ".debug.fileprovider",
|
linphone_file_provider: packageName + ".debug.fileprovider",
|
||||||
appLabel: "@string/app_name_debug",
|
appLabel: "@string/app_name_debug",
|
||||||
firebaseServiceEnabled: enableFirebaseService,
|
firebaseServiceEnabled: enableFirebaseService]
|
||||||
extractNativeLibs: "true"]
|
extractNativeLibs = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,9 +137,9 @@ android {
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
|
||||||
resValue "string", "linphone_app_branch", gitBranch.toString().trim()
|
resValue "string", "linphone_app_branch", gitBranch.toString().trim()
|
||||||
resValue "string", "sync_account_type", getPackageName() + ".sync"
|
resValue "string", "sync_account_type", packageName + ".sync"
|
||||||
resValue "string", "file_provider", getPackageName() + ".fileprovider"
|
resValue "string", "file_provider", packageName + ".fileprovider"
|
||||||
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"
|
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + packageName + ".provider.sip_address"
|
||||||
|
|
||||||
if (!firebaseAvailable) {
|
if (!firebaseAvailable) {
|
||||||
resValue "string", "gcm_defaultSenderId", "none"
|
resValue "string", "gcm_defaultSenderId", "none"
|
||||||
|
@ -169,9 +169,9 @@ android {
|
||||||
jniDebuggable true
|
jniDebuggable true
|
||||||
|
|
||||||
resValue "string", "linphone_app_branch", gitBranch.toString().trim()
|
resValue "string", "linphone_app_branch", gitBranch.toString().trim()
|
||||||
resValue "string", "sync_account_type", getPackageName() + ".sync"
|
resValue "string", "sync_account_type", packageName + ".sync"
|
||||||
resValue "string", "file_provider", getPackageName() + ".debug.fileprovider"
|
resValue "string", "file_provider", packageName + ".debug.fileprovider"
|
||||||
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"
|
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + packageName + ".provider.sip_address"
|
||||||
resValue "bool", "crashlytics_enabled", crashlyticsAvailable.toString()
|
resValue "bool", "crashlytics_enabled", crashlyticsAvailable.toString()
|
||||||
|
|
||||||
if (!firebaseAvailable) {
|
if (!firebaseAvailable) {
|
||||||
|
@ -193,28 +193,35 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace 'org.linphone'
|
namespace 'org.linphone'
|
||||||
|
packagingOptions {
|
||||||
|
jniLibs {
|
||||||
|
useLegacyPackaging extractNativeLibs
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.core:core-ktx:1.10.0'
|
implementation 'androidx.core:core-ktx:1.10.1'
|
||||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||||
implementation 'androidx.emoji2:emoji2:1.4.0-beta02'
|
|
||||||
implementation 'androidx.emoji2:emoji2-emojipicker:1.4.0-beta02'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
|
||||||
implementation 'androidx.media:media:1.6.0'
|
implementation 'androidx.media:media:1.6.0'
|
||||||
implementation "androidx.security:security-crypto-ktx:1.1.0-alpha06"
|
implementation "androidx.security:security-crypto-ktx:1.1.0-alpha06"
|
||||||
implementation "androidx.window:window:1.0.0"
|
implementation "androidx.window:window:1.1.0"
|
||||||
|
|
||||||
def nav_version = "2.5.3"
|
def emoji_version = "1.4.0-rc01"
|
||||||
|
implementation "androidx.emoji2:emoji2:$emoji_version"
|
||||||
|
implementation "androidx.emoji2:emoji2-emojipicker:$emoji_version"
|
||||||
|
|
||||||
|
def nav_version = "2.6.0"
|
||||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||||
|
|
||||||
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
|
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
implementation "androidx.gridlayout:gridlayout:1.0.0"
|
implementation "androidx.gridlayout:gridlayout:1.0.0"
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.0'
|
implementation 'androidx.recyclerview:recyclerview:1.3.1'
|
||||||
implementation 'androidx.drawerlayout:drawerlayout:1.2.0'
|
implementation 'androidx.drawerlayout:drawerlayout:1.2.0'
|
||||||
|
|
||||||
// https://github.com/material-components/material-components-android/blob/master/LICENSE Apache v2.0
|
// https://github.com/material-components/material-components-android/blob/master/LICENSE Apache v2.0
|
||||||
|
@ -223,7 +230,7 @@ dependencies {
|
||||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||||
|
|
||||||
// https://github.com/coil-kt/coil/blob/main/LICENSE.txt Apache v2.0
|
// https://github.com/coil-kt/coil/blob/main/LICENSE.txt Apache v2.0
|
||||||
def coil_version = "2.3.0"
|
def coil_version = "2.4.0"
|
||||||
implementation("io.coil-kt:coil:$coil_version")
|
implementation("io.coil-kt:coil:$coil_version")
|
||||||
implementation("io.coil-kt:coil-gif:$coil_version")
|
implementation("io.coil-kt:coil-gif:$coil_version")
|
||||||
implementation("io.coil-kt:coil-svg:$coil_version")
|
implementation("io.coil-kt:coil-svg:$coil_version")
|
||||||
|
@ -247,7 +254,7 @@ dependencies {
|
||||||
implementation 'org.linphone:linphone-sdk-android:5.3+'
|
implementation 'org.linphone:linphone-sdk-android:5.3+'
|
||||||
|
|
||||||
// Only enable leak canary prior to release
|
// Only enable leak canary prior to release
|
||||||
//debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
|
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
|
||||||
}
|
}
|
||||||
|
|
||||||
task generateContactsXml(type: Copy) {
|
task generateContactsXml(type: Copy) {
|
||||||
|
@ -257,7 +264,7 @@ task generateContactsXml(type: Copy) {
|
||||||
filter {
|
filter {
|
||||||
line -> line
|
line -> line
|
||||||
.replaceAll('%%AUTO_GENERATED%%', 'This file has been automatically generated, do not edit or commit !')
|
.replaceAll('%%AUTO_GENERATED%%', 'This file has been automatically generated, do not edit or commit !')
|
||||||
.replaceAll('%%PACKAGE_NAME%%', getPackageName())
|
.replaceAll('%%PACKAGE_NAME%%', packageName)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,6 +54,12 @@
|
||||||
<!-- Needed for foreground service
|
<!-- Needed for foreground service
|
||||||
(https://developer.android.com/guide/components/foreground-services) -->
|
(https://developer.android.com/guide/components/foreground-services) -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||||
|
<!-- Needed for Android 14
|
||||||
|
https://developer.android.com/about/versions/14/behavior-changes-14#fgs-types -->
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".LinphoneApplication"
|
android:name=".LinphoneApplication"
|
||||||
|
@ -63,7 +69,6 @@
|
||||||
android:label="${appLabel}"
|
android:label="${appLabel}"
|
||||||
android:localeConfig="@xml/locales_config"
|
android:localeConfig="@xml/locales_config"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:extractNativeLibs="${extractNativeLibs}"
|
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:allowNativeHeapPointerTagging="false">
|
android:allowNativeHeapPointerTagging="false">
|
||||||
|
|
||||||
|
@ -121,6 +126,7 @@
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<action android:name="android.intent.action.DIAL" />
|
<action android:name="android.intent.action.DIAL" />
|
||||||
<action android:name="android.intent.action.CALL" />
|
<action android:name="android.intent.action.CALL" />
|
||||||
|
<action android:name="android.intent.action.CALL_BUTTON" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="tel" />
|
<data android:scheme="tel" />
|
||||||
|
@ -128,12 +134,6 @@
|
||||||
<data android:scheme="sips" />
|
<data android:scheme="sips" />
|
||||||
<data android:scheme="linphone" />
|
<data android:scheme="linphone" />
|
||||||
<data android:scheme="sip-linphone" />
|
<data android:scheme="sip-linphone" />
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
|
||||||
<data android:scheme="linphone-config" />
|
<data android:scheme="linphone-config" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
@ -169,7 +169,14 @@
|
||||||
<service
|
<service
|
||||||
android:name=".core.CoreService"
|
android:name=".core.CoreService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="phoneCall|camera|microphone"
|
android:foregroundServiceType="phoneCall|camera|microphone|dataSync"
|
||||||
|
android:stopWithTask="false"
|
||||||
|
android:label="@string/app_name" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name="org.linphone.core.tools.service.PushService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync"
|
||||||
android:stopWithTask="false"
|
android:stopWithTask="false"
|
||||||
android:label="@string/app_name" />
|
android:label="@string/app_name" />
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,8 @@ class LinphoneApplication : Application(), ImageLoaderFactory {
|
||||||
context: Context,
|
context: Context,
|
||||||
pushReceived: Boolean = false,
|
pushReceived: Boolean = false,
|
||||||
service: CoreService? = null,
|
service: CoreService? = null,
|
||||||
useAutoStartDescription: Boolean = false
|
useAutoStartDescription: Boolean = false,
|
||||||
|
skipCoreStart: Boolean = false
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (::coreContext.isInitialized && !coreContext.stopped) {
|
if (::coreContext.isInitialized && !coreContext.stopped) {
|
||||||
Log.d("[Application] Skipping Core creation (push received? $pushReceived)")
|
Log.d("[Application] Skipping Core creation (push received? $pushReceived)")
|
||||||
|
@ -96,7 +97,9 @@ class LinphoneApplication : Application(), ImageLoaderFactory {
|
||||||
service,
|
service,
|
||||||
useAutoStartDescription
|
useAutoStartDescription
|
||||||
)
|
)
|
||||||
|
if (!skipCoreStart) {
|
||||||
coreContext.start()
|
coreContext.start()
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,9 @@ abstract class GenericFragment<T : ViewDataBinding> : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (ise: IllegalStateException) {
|
} catch (ise: IllegalStateException) {
|
||||||
Log.e("[Generic Fragment] ${getFragmentRealClassName()} Can't go back: $ise")
|
Log.e(
|
||||||
|
"[Generic Fragment] ${getFragmentRealClassName()}.handleOnBackPressed() Can't go back: $ise"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -137,7 +139,8 @@ abstract class GenericFragment<T : ViewDataBinding> : Fragment() {
|
||||||
try {
|
try {
|
||||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||||
} catch (ise: IllegalStateException) {
|
} catch (ise: IllegalStateException) {
|
||||||
Log.e("[Generic Fragment] ${getFragmentRealClassName()} can't go back: $ise")
|
Log.w("[Generic Fragment] ${getFragmentRealClassName()}.goBack() can't go back: $ise")
|
||||||
|
onBackPressedCallback.handleOnBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,10 @@ class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragment
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.prefix.observe(viewLifecycleOwner) { internationalPrefix ->
|
||||||
|
viewModel.getCountryNameFromPrefix(internationalPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.goToSmsValidationEvent.observe(
|
viewModel.goToSmsValidationEvent.observe(
|
||||||
viewLifecycleOwner
|
viewLifecycleOwner
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -29,7 +29,7 @@ import org.linphone.activities.assistant.adapters.CountryPickerAdapter
|
||||||
import org.linphone.core.DialPlan
|
import org.linphone.core.DialPlan
|
||||||
import org.linphone.databinding.AssistantCountryPickerFragmentBinding
|
import org.linphone.databinding.AssistantCountryPickerFragmentBinding
|
||||||
|
|
||||||
class CountryPickerFragment() : DialogFragment() {
|
class CountryPickerFragment : DialogFragment() {
|
||||||
private var _binding: AssistantCountryPickerFragmentBinding? = null
|
private var _binding: AssistantCountryPickerFragmentBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
private lateinit var adapter: CountryPickerAdapter
|
private lateinit var adapter: CountryPickerAdapter
|
||||||
|
|
|
@ -62,6 +62,10 @@ class PhoneAccountCreationFragment :
|
||||||
countryPickerFragment.show(childFragmentManager, "CountryPicker")
|
countryPickerFragment.show(childFragmentManager, "CountryPicker")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.prefix.observe(viewLifecycleOwner) { internationalPrefix ->
|
||||||
|
viewModel.getCountryNameFromPrefix(internationalPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.goToSmsValidationEvent.observe(
|
viewModel.goToSmsValidationEvent.observe(
|
||||||
viewLifecycleOwner
|
viewLifecycleOwner
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -73,6 +73,10 @@ class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountL
|
||||||
countryPickerFragment.show(childFragmentManager, "CountryPicker")
|
countryPickerFragment.show(childFragmentManager, "CountryPicker")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.prefix.observe(viewLifecycleOwner) { internationalPrefix ->
|
||||||
|
viewModel.getCountryNameFromPrefix(internationalPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.goToSmsValidationEvent.observe(
|
viewModel.goToSmsValidationEvent.observe(
|
||||||
viewLifecycleOwner
|
viewLifecycleOwner
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -34,6 +34,7 @@ import org.linphone.activities.assistant.viewmodels.PhoneAccountValidationViewMo
|
||||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||||
import org.linphone.activities.navigateToAccountSettings
|
import org.linphone.activities.navigateToAccountSettings
|
||||||
import org.linphone.activities.navigateToEchoCancellerCalibration
|
import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||||
|
import org.linphone.compatibility.Compatibility
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.databinding.AssistantPhoneAccountValidationFragmentBinding
|
import org.linphone.databinding.AssistantPhoneAccountValidationFragmentBinding
|
||||||
|
|
||||||
|
@ -113,7 +114,7 @@ class PhoneAccountValidationFragment : GenericFragment<AssistantPhoneAccountVali
|
||||||
"[Assistant] [Phone Account Validation] Found 4 digits as primary clip in clipboard, using it and clear it"
|
"[Assistant] [Phone Account Validation] Found 4 digits as primary clip in clipboard, using it and clear it"
|
||||||
)
|
)
|
||||||
viewModel.code.value = clip
|
viewModel.code.value = clip
|
||||||
clipboard.clearPrimaryClip()
|
Compatibility.clearClipboard(clipboard)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@
|
||||||
package org.linphone.activities.assistant.viewmodels
|
package org.linphone.activities.assistant.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import org.linphone.activities.assistant.fragments.CountryPickerFragment
|
import org.linphone.activities.assistant.fragments.CountryPickerFragment
|
||||||
import org.linphone.core.AccountCreator
|
import org.linphone.core.AccountCreator
|
||||||
import org.linphone.core.DialPlan
|
import org.linphone.core.DialPlan
|
||||||
|
@ -33,6 +32,7 @@ abstract class AbstractPhoneViewModel(accountCreator: AccountCreator) :
|
||||||
CountryPickerFragment.CountryPickedListener {
|
CountryPickerFragment.CountryPickedListener {
|
||||||
|
|
||||||
val prefix = MutableLiveData<String>()
|
val prefix = MutableLiveData<String>()
|
||||||
|
val prefixError = MutableLiveData<String>()
|
||||||
|
|
||||||
val phoneNumber = MutableLiveData<String>()
|
val phoneNumber = MutableLiveData<String>()
|
||||||
val phoneNumberError = MutableLiveData<String>()
|
val phoneNumberError = MutableLiveData<String>()
|
||||||
|
@ -49,7 +49,10 @@ abstract class AbstractPhoneViewModel(accountCreator: AccountCreator) :
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isPhoneNumberOk(): Boolean {
|
fun isPhoneNumberOk(): Boolean {
|
||||||
return prefix.value.orEmpty().isNotEmpty() && phoneNumber.value.orEmpty().isNotEmpty() && phoneNumberError.value.orEmpty().isEmpty()
|
return prefix.value.orEmpty().length > 1 && // Not just '+' character
|
||||||
|
prefixError.value.orEmpty().isEmpty() &&
|
||||||
|
phoneNumber.value.orEmpty().isNotEmpty() &&
|
||||||
|
phoneNumberError.value.orEmpty().isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFromPhoneNumberAndOrDialPlan(number: String?, dialPlan: DialPlan?) {
|
fun updateFromPhoneNumberAndOrDialPlan(number: String?, dialPlan: DialPlan?) {
|
||||||
|
@ -70,7 +73,7 @@ abstract class AbstractPhoneViewModel(accountCreator: AccountCreator) :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCountryNameFromPrefix(prefix: String?) {
|
fun getCountryNameFromPrefix(prefix: String?) {
|
||||||
if (!prefix.isNullOrEmpty()) {
|
if (!prefix.isNullOrEmpty()) {
|
||||||
val countryCode = if (prefix.first() == '+') prefix.substring(1) else prefix
|
val countryCode = if (prefix.first() == '+') prefix.substring(1) else prefix
|
||||||
val dialPlan = PhoneNumberUtils.getDialPlanFromCountryCallingPrefix(countryCode)
|
val dialPlan = PhoneNumberUtils.getDialPlanFromCountryCallingPrefix(countryCode)
|
||||||
|
|
|
@ -46,7 +46,7 @@ class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewM
|
||||||
val password = MutableLiveData<String>()
|
val password = MutableLiveData<String>()
|
||||||
val passwordError = MutableLiveData<String>()
|
val passwordError = MutableLiveData<String>()
|
||||||
|
|
||||||
val loginEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
val loginEnabled = MediatorLiveData<Boolean>()
|
||||||
|
|
||||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
@ -140,6 +140,9 @@ class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewM
|
||||||
loginEnabled.addSource(phoneNumberError) {
|
loginEnabled.addSource(phoneNumberError) {
|
||||||
loginEnabled.value = isLoginButtonEnabled()
|
loginEnabled.value = isLoginButtonEnabled()
|
||||||
}
|
}
|
||||||
|
loginEnabled.addSource(prefixError) {
|
||||||
|
loginEnabled.value = isLoginButtonEnabled()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
@ -149,6 +152,7 @@ class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewM
|
||||||
|
|
||||||
override fun onFlexiApiTokenReceived() {
|
override fun onFlexiApiTokenReceived() {
|
||||||
Log.i("[Assistant] [Account Login] Using FlexiAPI auth token [${accountCreator.token}]")
|
Log.i("[Assistant] [Account Login] Using FlexiAPI auth token [${accountCreator.token}]")
|
||||||
|
waitForServerAnswer.value = false
|
||||||
loginWithPhoneNumber()
|
loginWithPhoneNumber()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,10 +165,19 @@ class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewM
|
||||||
fun removeInvalidProxyConfig() {
|
fun removeInvalidProxyConfig() {
|
||||||
val account = accountToCheck
|
val account = accountToCheck
|
||||||
account ?: return
|
account ?: return
|
||||||
|
|
||||||
|
val core = coreContext.core
|
||||||
val authInfo = account.findAuthInfo()
|
val authInfo = account.findAuthInfo()
|
||||||
if (authInfo != null) coreContext.core.removeAuthInfo(authInfo)
|
if (authInfo != null) core.removeAuthInfo(authInfo)
|
||||||
coreContext.core.removeAccount(account)
|
core.removeAccount(account)
|
||||||
accountToCheck = null
|
accountToCheck = null
|
||||||
|
|
||||||
|
// Make sure there is a valid default account
|
||||||
|
val accounts = core.accountList
|
||||||
|
if (accounts.isNotEmpty() && core.defaultAccount == null) {
|
||||||
|
core.defaultAccount = accounts.first()
|
||||||
|
core.refreshRegisters()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun continueEvenIfInvalidCredentials() {
|
fun continueEvenIfInvalidCredentials() {
|
||||||
|
|
|
@ -51,16 +51,16 @@ class EchoCancellerCalibrationViewModel : ViewModel() {
|
||||||
coreContext.core.removeListener(listener)
|
coreContext.core.removeListener(listener)
|
||||||
when (status) {
|
when (status) {
|
||||||
EcCalibratorStatus.DoneNoEcho -> {
|
EcCalibratorStatus.DoneNoEcho -> {
|
||||||
Log.i("[Echo Canceller Calibration] Done, no echo")
|
Log.i("[Assistant] [Echo Canceller Calibration] Done, no echo")
|
||||||
}
|
}
|
||||||
EcCalibratorStatus.Done -> {
|
EcCalibratorStatus.Done -> {
|
||||||
Log.i("[Echo Canceller Calibration] Done, delay is ${delay}ms")
|
Log.i("[Assistant] [Echo Canceller Calibration] Done, delay is ${delay}ms")
|
||||||
}
|
}
|
||||||
EcCalibratorStatus.Failed -> {
|
EcCalibratorStatus.Failed -> {
|
||||||
Log.w("[Echo Canceller Calibration] Failed")
|
Log.w("[Assistant] [Echo Canceller Calibration] Failed")
|
||||||
}
|
}
|
||||||
EcCalibratorStatus.InProgress -> {
|
EcCalibratorStatus.InProgress -> {
|
||||||
Log.i("[Echo Canceller Calibration] In progress")
|
Log.i("[Assistant] [Echo Canceller Calibration] In progress")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
echoCalibrationTerminated.value = Event(true)
|
echoCalibrationTerminated.value = Event(true)
|
||||||
|
|
|
@ -72,7 +72,7 @@ class EmailAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPu
|
||||||
status: AccountCreator.Status,
|
status: AccountCreator.Status,
|
||||||
response: String?
|
response: String?
|
||||||
) {
|
) {
|
||||||
Log.i("[Account Creation] onIsAccountExist status is $status")
|
Log.i("[Assistant] [Account Creation] onIsAccountExist status is $status")
|
||||||
when (status) {
|
when (status) {
|
||||||
AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> {
|
AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> {
|
||||||
waitForServerAnswer.value = false
|
waitForServerAnswer.value = false
|
||||||
|
@ -99,7 +99,7 @@ class EmailAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPu
|
||||||
status: AccountCreator.Status,
|
status: AccountCreator.Status,
|
||||||
response: String?
|
response: String?
|
||||||
) {
|
) {
|
||||||
Log.i("[Account Creation] onCreateAccount status is $status")
|
Log.i("[Assistant] [Account Creation] onCreateAccount status is $status")
|
||||||
waitForServerAnswer.value = false
|
waitForServerAnswer.value = false
|
||||||
|
|
||||||
when (status) {
|
when (status) {
|
||||||
|
@ -149,11 +149,11 @@ class EmailAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPu
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFlexiApiTokenReceived() {
|
override fun onFlexiApiTokenReceived() {
|
||||||
Log.i("[Account Creation] Using FlexiAPI auth token [${accountCreator.token}]")
|
Log.i("[Assistant] [Account Creation] Using FlexiAPI auth token [${accountCreator.token}]")
|
||||||
|
|
||||||
waitForServerAnswer.value = true
|
waitForServerAnswer.value = true
|
||||||
val status = accountCreator.isAccountExist
|
val status = accountCreator.isAccountExist
|
||||||
Log.i("[Account Creation] Account exists returned $status")
|
Log.i("[Assistant] [Account Creation] Account exists returned $status")
|
||||||
if (status != AccountCreator.Status.RequestOk) {
|
if (status != AccountCreator.Status.RequestOk) {
|
||||||
waitForServerAnswer.value = false
|
waitForServerAnswer.value = false
|
||||||
onErrorEvent.value = Event("Error: ${status.name}")
|
onErrorEvent.value = Event("Error: ${status.name}")
|
||||||
|
@ -161,7 +161,7 @@ class EmailAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPu
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFlexiApiTokenRequestError() {
|
override fun onFlexiApiTokenRequestError() {
|
||||||
Log.e("[Account Creation] Failed to get an auth token from FlexiAPI")
|
Log.e("[Assistant] [Account Creation] Failed to get an auth token from FlexiAPI")
|
||||||
waitForServerAnswer.value = false
|
waitForServerAnswer.value = false
|
||||||
onErrorEvent.value = Event("Error: Failed to get an auth token from account manager server")
|
onErrorEvent.value = Event("Error: Failed to get an auth token from account manager server")
|
||||||
}
|
}
|
||||||
|
@ -175,11 +175,11 @@ class EmailAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPu
|
||||||
val token = accountCreator.token.orEmpty()
|
val token = accountCreator.token.orEmpty()
|
||||||
if (token.isNotEmpty()) {
|
if (token.isNotEmpty()) {
|
||||||
Log.i(
|
Log.i(
|
||||||
"[Account Creation] We already have an auth token from FlexiAPI [$token], continue"
|
"[Assistant] [Account Creation] We already have an auth token from FlexiAPI [$token], continue"
|
||||||
)
|
)
|
||||||
onFlexiApiTokenReceived()
|
onFlexiApiTokenReceived()
|
||||||
} else {
|
} else {
|
||||||
Log.i("[Account Creation] Requesting an auth token from FlexiAPI")
|
Log.i("[Assistant] [Account Creation] Requesting an auth token from FlexiAPI")
|
||||||
waitForServerAnswer.value = true
|
waitForServerAnswer.value = true
|
||||||
requestFlexiApiToken()
|
requestFlexiApiToken()
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ class EmailAccountValidationViewModel(val accountCreator: AccountCreator) : View
|
||||||
status: AccountCreator.Status,
|
status: AccountCreator.Status,
|
||||||
response: String?
|
response: String?
|
||||||
) {
|
) {
|
||||||
Log.i("[Account Validation] onIsAccountActivated status is $status")
|
Log.i("[Assistant] [Account Validation] onIsAccountActivated status is $status")
|
||||||
waitForServerAnswer.value = false
|
waitForServerAnswer.value = false
|
||||||
|
|
||||||
when (status) {
|
when (status) {
|
||||||
|
|
|
@ -108,10 +108,19 @@ class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewMo
|
||||||
fun removeInvalidProxyConfig() {
|
fun removeInvalidProxyConfig() {
|
||||||
val account = accountToCheck
|
val account = accountToCheck
|
||||||
account ?: return
|
account ?: return
|
||||||
|
|
||||||
|
val core = coreContext.core
|
||||||
val authInfo = account.findAuthInfo()
|
val authInfo = account.findAuthInfo()
|
||||||
if (authInfo != null) coreContext.core.removeAuthInfo(authInfo)
|
if (authInfo != null) core.removeAuthInfo(authInfo)
|
||||||
coreContext.core.removeAccount(account)
|
core.removeAccount(account)
|
||||||
accountToCheck = null
|
accountToCheck = null
|
||||||
|
|
||||||
|
// Make sure there is a valid default account
|
||||||
|
val accounts = core.accountList
|
||||||
|
if (accounts.isNotEmpty() && core.defaultAccount == null) {
|
||||||
|
core.defaultAccount = accounts.first()
|
||||||
|
core.refreshRegisters()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun continueEvenIfInvalidCredentials() {
|
fun continueEvenIfInvalidCredentials() {
|
||||||
|
@ -143,6 +152,8 @@ class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewMo
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isLoginButtonEnabled(): Boolean {
|
private fun isLoginButtonEnabled(): Boolean {
|
||||||
return username.value.orEmpty().isNotEmpty() && domain.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty()
|
return username.value.orEmpty().isNotEmpty() &&
|
||||||
|
domain.value.orEmpty().isNotEmpty() &&
|
||||||
|
password.value.orEmpty().isNotEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
|
||||||
status: AccountCreator.Status,
|
status: AccountCreator.Status,
|
||||||
response: String?
|
response: String?
|
||||||
) {
|
) {
|
||||||
Log.i("[Phone Account Creation] onIsAccountExist status is $status")
|
Log.i("[Assistant] [Phone Account Creation] onIsAccountExist status is $status")
|
||||||
when (status) {
|
when (status) {
|
||||||
AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> {
|
AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> {
|
||||||
waitForServerAnswer.value = false
|
waitForServerAnswer.value = false
|
||||||
|
@ -92,7 +92,7 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
|
||||||
status: AccountCreator.Status,
|
status: AccountCreator.Status,
|
||||||
response: String?
|
response: String?
|
||||||
) {
|
) {
|
||||||
Log.i("[Phone Account Creation] onIsAliasUsed status is $status")
|
Log.i("[Assistant] [Phone Account Creation] onIsAliasUsed status is $status")
|
||||||
when (status) {
|
when (status) {
|
||||||
AccountCreator.Status.AliasExist -> {
|
AccountCreator.Status.AliasExist -> {
|
||||||
waitForServerAnswer.value = false
|
waitForServerAnswer.value = false
|
||||||
|
@ -114,7 +114,9 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
|
||||||
}
|
}
|
||||||
AccountCreator.Status.AliasNotExist -> {
|
AccountCreator.Status.AliasNotExist -> {
|
||||||
val createAccountStatus = creator.createAccount()
|
val createAccountStatus = creator.createAccount()
|
||||||
Log.i("[Phone Account Creation] createAccount returned $createAccountStatus")
|
Log.i(
|
||||||
|
"[Assistant] [Phone Account Creation] createAccount returned $createAccountStatus"
|
||||||
|
)
|
||||||
if (createAccountStatus != AccountCreator.Status.RequestOk) {
|
if (createAccountStatus != AccountCreator.Status.RequestOk) {
|
||||||
waitForServerAnswer.value = false
|
waitForServerAnswer.value = false
|
||||||
onErrorEvent.value = Event("Error: ${status.name}")
|
onErrorEvent.value = Event("Error: ${status.name}")
|
||||||
|
@ -132,7 +134,7 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
|
||||||
status: AccountCreator.Status,
|
status: AccountCreator.Status,
|
||||||
response: String?
|
response: String?
|
||||||
) {
|
) {
|
||||||
Log.i("[Phone Account Creation] onCreateAccount status is $status")
|
Log.i("[Assistant] [Phone Account Creation] onCreateAccount status is $status")
|
||||||
waitForServerAnswer.value = false
|
waitForServerAnswer.value = false
|
||||||
when (status) {
|
when (status) {
|
||||||
AccountCreator.Status.AccountCreated -> {
|
AccountCreator.Status.AccountCreated -> {
|
||||||
|
@ -173,6 +175,9 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
|
||||||
createEnabled.addSource(phoneNumberError) {
|
createEnabled.addSource(phoneNumberError) {
|
||||||
createEnabled.value = isCreateButtonEnabled()
|
createEnabled.value = isCreateButtonEnabled()
|
||||||
}
|
}
|
||||||
|
createEnabled.addSource(prefixError) {
|
||||||
|
createEnabled.value = isCreateButtonEnabled()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
@ -181,9 +186,22 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFlexiApiTokenReceived() {
|
override fun onFlexiApiTokenReceived() {
|
||||||
Log.i("[Phone Account Creation] Using FlexiAPI auth token [${accountCreator.token}]")
|
Log.i(
|
||||||
|
"[Assistant] [Phone Account Creation] Using FlexiAPI auth token [${accountCreator.token}]"
|
||||||
|
)
|
||||||
accountCreator.displayName = displayName.value
|
accountCreator.displayName = displayName.value
|
||||||
|
|
||||||
|
val result = AccountCreator.PhoneNumberStatus.fromInt(
|
||||||
accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
|
accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
|
||||||
|
)
|
||||||
|
if (result != AccountCreator.PhoneNumberStatus.Ok) {
|
||||||
|
Log.e(
|
||||||
|
"[Assistant] [Phone Account Creation] Error [$result] setting the phone number: ${phoneNumber.value} with prefix: ${prefix.value}"
|
||||||
|
)
|
||||||
|
phoneNumberError.value = result.name
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.i("[Assistant] [Phone Account Creation] Phone number is ${accountCreator.phoneNumber}")
|
||||||
|
|
||||||
if (useUsername.value == true) {
|
if (useUsername.value == true) {
|
||||||
accountCreator.username = username.value
|
accountCreator.username = username.value
|
||||||
|
@ -199,14 +217,14 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFlexiApiTokenRequestError() {
|
override fun onFlexiApiTokenRequestError() {
|
||||||
Log.e("[Phone Account Creation] Failed to get an auth token from FlexiAPI")
|
Log.e("[Assistant] [Phone Account Creation] Failed to get an auth token from FlexiAPI")
|
||||||
waitForServerAnswer.value = false
|
waitForServerAnswer.value = false
|
||||||
onErrorEvent.value = Event("Error: Failed to get an auth token from account manager server")
|
onErrorEvent.value = Event("Error: Failed to get an auth token from account manager server")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkUsername() {
|
private fun checkUsername() {
|
||||||
val status = accountCreator.isAccountExist
|
val status = accountCreator.isAccountExist
|
||||||
Log.i("[Phone Account Creation] isAccountExist returned $status")
|
Log.i("[Assistant] [Phone Account Creation] isAccountExist returned $status")
|
||||||
if (status != AccountCreator.Status.RequestOk) {
|
if (status != AccountCreator.Status.RequestOk) {
|
||||||
waitForServerAnswer.value = false
|
waitForServerAnswer.value = false
|
||||||
onErrorEvent.value = Event("Error: ${status.name}")
|
onErrorEvent.value = Event("Error: ${status.name}")
|
||||||
|
@ -215,7 +233,7 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
|
||||||
|
|
||||||
private fun checkPhoneNumber() {
|
private fun checkPhoneNumber() {
|
||||||
val status = accountCreator.isAliasUsed
|
val status = accountCreator.isAliasUsed
|
||||||
Log.i("[Phone Account Creation] isAliasUsed returned $status")
|
Log.i("[Assistant] [Phone Account Creation] isAliasUsed returned $status")
|
||||||
if (status != AccountCreator.Status.RequestOk) {
|
if (status != AccountCreator.Status.RequestOk) {
|
||||||
waitForServerAnswer.value = false
|
waitForServerAnswer.value = false
|
||||||
onErrorEvent.value = Event("Error: ${status.name}")
|
onErrorEvent.value = Event("Error: ${status.name}")
|
||||||
|
@ -226,11 +244,11 @@ class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPh
|
||||||
val token = accountCreator.token.orEmpty()
|
val token = accountCreator.token.orEmpty()
|
||||||
if (token.isNotEmpty()) {
|
if (token.isNotEmpty()) {
|
||||||
Log.i(
|
Log.i(
|
||||||
"[Phone Account Creation] We already have an auth token from FlexiAPI [$token], continue"
|
"[Assistant] [Phone Account Creation] We already have an auth token from FlexiAPI [$token], continue"
|
||||||
)
|
)
|
||||||
onFlexiApiTokenReceived()
|
onFlexiApiTokenReceived()
|
||||||
} else {
|
} else {
|
||||||
Log.i("[Phone Account Creation] Requesting an auth token from FlexiAPI")
|
Log.i("[Assistant] [Phone Account Creation] Requesting an auth token from FlexiAPI")
|
||||||
waitForServerAnswer.value = true
|
waitForServerAnswer.value = true
|
||||||
requestFlexiApiToken()
|
requestFlexiApiToken()
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,12 +60,12 @@ class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPho
|
||||||
status: AccountCreator.Status,
|
status: AccountCreator.Status,
|
||||||
response: String?
|
response: String?
|
||||||
) {
|
) {
|
||||||
Log.i("[Phone Account Linking] onIsAliasUsed status is $status")
|
Log.i("[Assistant] [Phone Account Linking] onIsAliasUsed status is $status")
|
||||||
|
|
||||||
when (status) {
|
when (status) {
|
||||||
AccountCreator.Status.AliasNotExist -> {
|
AccountCreator.Status.AliasNotExist -> {
|
||||||
if (creator.linkAccount() != AccountCreator.Status.RequestOk) {
|
if (creator.linkAccount() != AccountCreator.Status.RequestOk) {
|
||||||
Log.e("[Phone Account Linking] linkAccount status is $status")
|
Log.e("[Assistant] [Phone Account Linking] linkAccount status is $status")
|
||||||
waitForServerAnswer.value = false
|
waitForServerAnswer.value = false
|
||||||
onErrorEvent.value = Event("Error: ${status.name}")
|
onErrorEvent.value = Event("Error: ${status.name}")
|
||||||
}
|
}
|
||||||
|
@ -86,7 +86,7 @@ class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPho
|
||||||
status: AccountCreator.Status,
|
status: AccountCreator.Status,
|
||||||
response: String?
|
response: String?
|
||||||
) {
|
) {
|
||||||
Log.i("[Phone Account Linking] onLinkAccount status is $status")
|
Log.i("[Assistant] [Phone Account Linking] onLinkAccount status is $status")
|
||||||
waitForServerAnswer.value = false
|
waitForServerAnswer.value = false
|
||||||
|
|
||||||
when (status) {
|
when (status) {
|
||||||
|
@ -113,6 +113,9 @@ class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPho
|
||||||
linkEnabled.addSource(phoneNumberError) {
|
linkEnabled.addSource(phoneNumberError) {
|
||||||
linkEnabled.value = isLinkButtonEnabled()
|
linkEnabled.value = isLinkButtonEnabled()
|
||||||
}
|
}
|
||||||
|
linkEnabled.addSource(prefixError) {
|
||||||
|
linkEnabled.value = isLinkButtonEnabled()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
@ -123,10 +126,10 @@ class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPho
|
||||||
override fun onFlexiApiTokenReceived() {
|
override fun onFlexiApiTokenReceived() {
|
||||||
accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
|
accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
|
||||||
accountCreator.username = username.value
|
accountCreator.username = username.value
|
||||||
Log.i("[Phone Account Linking] Phone number is ${accountCreator.phoneNumber}")
|
Log.i("[Assistant] [Phone Account Linking] Phone number is ${accountCreator.phoneNumber}")
|
||||||
|
|
||||||
val status: AccountCreator.Status = accountCreator.isAliasUsed
|
val status: AccountCreator.Status = accountCreator.isAliasUsed
|
||||||
Log.i("[Phone Account Linking] isAliasUsed returned $status")
|
Log.i("[Assistant] [Phone Account Linking] isAliasUsed returned $status")
|
||||||
if (status != AccountCreator.Status.RequestOk) {
|
if (status != AccountCreator.Status.RequestOk) {
|
||||||
waitForServerAnswer.value = false
|
waitForServerAnswer.value = false
|
||||||
onErrorEvent.value = Event("Error: ${status.name}")
|
onErrorEvent.value = Event("Error: ${status.name}")
|
||||||
|
@ -134,12 +137,12 @@ class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPho
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFlexiApiTokenRequestError() {
|
override fun onFlexiApiTokenRequestError() {
|
||||||
Log.e("[Phone Account Linking] Failed to get an auth token from FlexiAPI")
|
Log.e("[Assistant] [Phone Account Linking] Failed to get an auth token from FlexiAPI")
|
||||||
waitForServerAnswer.value = false
|
waitForServerAnswer.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun link() {
|
fun link() {
|
||||||
Log.i("[Phone Account Linking] Requesting an auth token from FlexiAPI")
|
Log.i("[Assistant] [Phone Account Linking] Requesting an auth token from FlexiAPI")
|
||||||
waitForServerAnswer.value = true
|
waitForServerAnswer.value = true
|
||||||
requestFlexiApiToken()
|
requestFlexiApiToken()
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ class QrCodeViewModel : ViewModel() {
|
||||||
|
|
||||||
private val listener = object : CoreListenerStub() {
|
private val listener = object : CoreListenerStub() {
|
||||||
override fun onQrcodeFound(core: Core, result: String?) {
|
override fun onQrcodeFound(core: Core, result: String?) {
|
||||||
Log.i("[QR Code] Found [$result]")
|
Log.i("[Assistant] [QR Code] Found [$result]")
|
||||||
if (result != null) qrCodeFoundEvent.postValue(Event(result))
|
if (result != null) qrCodeFoundEvent.postValue(Event(result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ class QrCodeViewModel : ViewModel() {
|
||||||
|
|
||||||
for (camera in coreContext.core.videoDevicesList) {
|
for (camera in coreContext.core.videoDevicesList) {
|
||||||
if (camera.contains("Back")) {
|
if (camera.contains("Back")) {
|
||||||
Log.i("[QR Code] Found back facing camera: $camera")
|
Log.i("[Assistant] [QR Code] Found back facing camera: $camera")
|
||||||
coreContext.core.videoDevice = camera
|
coreContext.core.videoDevice = camera
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ class QrCodeViewModel : ViewModel() {
|
||||||
|
|
||||||
val first = coreContext.core.videoDevicesList.firstOrNull()
|
val first = coreContext.core.videoDevicesList.firstOrNull()
|
||||||
if (first != null) {
|
if (first != null) {
|
||||||
Log.i("[QR Code] Using first camera found: $first")
|
Log.i("[Assistant] [QR Code] Using first camera found: $first")
|
||||||
coreContext.core.videoDevice = first
|
coreContext.core.videoDevice = first
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,7 @@ class RemoteProvisioningViewModel : ViewModel() {
|
||||||
fun fetchAndApply() {
|
fun fetchAndApply() {
|
||||||
val url = urlToFetch.value.orEmpty()
|
val url = urlToFetch.value.orEmpty()
|
||||||
coreContext.core.provisioningUri = url
|
coreContext.core.provisioningUri = url
|
||||||
Log.w("[Remote Provisioning] Url set to [$url], restarting Core")
|
Log.w("[Assistant] [Remote Provisioning] Url set to [$url], restarting Core")
|
||||||
fetchInProgress.value = true
|
fetchInProgress.value = true
|
||||||
coreContext.core.stop()
|
coreContext.core.stop()
|
||||||
coreContext.core.start()
|
coreContext.core.start()
|
||||||
|
|
|
@ -35,6 +35,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.doOnAttach
|
import androidx.core.view.doOnAttach
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.fragment.app.FragmentContainerView
|
import androidx.fragment.app.FragmentContainerView
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
@ -58,6 +59,10 @@ import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||||
import org.linphone.activities.navigateToDialer
|
import org.linphone.activities.navigateToDialer
|
||||||
import org.linphone.compatibility.Compatibility
|
import org.linphone.compatibility.Compatibility
|
||||||
import org.linphone.contact.ContactsUpdatedListenerStub
|
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||||
|
import org.linphone.core.AuthInfo
|
||||||
|
import org.linphone.core.AuthMethod
|
||||||
|
import org.linphone.core.Core
|
||||||
|
import org.linphone.core.CoreListenerStub
|
||||||
import org.linphone.core.CorePreferences
|
import org.linphone.core.CorePreferences
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.databinding.MainActivityBinding
|
import org.linphone.databinding.MainActivityBinding
|
||||||
|
@ -108,6 +113,25 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
private var shouldTabsBeVisibleDependingOnDestination = true
|
private var shouldTabsBeVisibleDependingOnDestination = true
|
||||||
private var shouldTabsBeVisibleDueToOrientationAndKeyboard = true
|
private var shouldTabsBeVisibleDueToOrientationAndKeyboard = true
|
||||||
|
|
||||||
|
private val authenticationRequestedEvent: MutableLiveData<Event<AuthInfo>> by lazy {
|
||||||
|
MutableLiveData<Event<AuthInfo>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val coreListener: CoreListenerStub = object : CoreListenerStub() {
|
||||||
|
override fun onAuthenticationRequested(core: Core, authInfo: AuthInfo, method: AuthMethod) {
|
||||||
|
if (authInfo.username == null || authInfo.domain == null || authInfo.realm == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.w(
|
||||||
|
"[Main Activity] Authentication requested for account [${authInfo.username}@${authInfo.domain}] with realm [${authInfo.realm}] using method [$method]"
|
||||||
|
)
|
||||||
|
authenticationRequestedEvent.value = Event(authInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val keyboardVisibilityListeners = arrayListOf<AppUtils.KeyboardVisibilityListener>()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@ -143,6 +167,14 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authenticationRequestedEvent.observe(
|
||||||
|
this
|
||||||
|
) {
|
||||||
|
it.consume { authInfo ->
|
||||||
|
showAuthenticationRequestedDialog(authInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (coreContext.core.accountList.isEmpty()) {
|
if (coreContext.core.accountList.isEmpty()) {
|
||||||
if (corePreferences.firstStart) {
|
if (corePreferences.firstStart) {
|
||||||
startActivity(Intent(this, AssistantActivity::class.java))
|
startActivity(Intent(this, AssistantActivity::class.java))
|
||||||
|
@ -174,9 +206,11 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
coreContext.contactsManager.addListener(listener)
|
coreContext.contactsManager.addListener(listener)
|
||||||
|
coreContext.core.addListener(coreListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
coreContext.core.removeListener(coreListener)
|
||||||
coreContext.contactsManager.removeListener(listener)
|
coreContext.contactsManager.removeListener(listener)
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
@ -205,13 +239,17 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
registerComponentCallbacks(componentCallbacks)
|
registerComponentCallbacks(componentCallbacks)
|
||||||
findNavController(R.id.nav_host_fragment).addOnDestinationChangedListener(this)
|
findNavController(R.id.nav_host_fragment).addOnDestinationChangedListener(this)
|
||||||
|
|
||||||
binding.rootCoordinatorLayout.addKeyboardInsetListener { keyboardVisible ->
|
binding.rootCoordinatorLayout.setKeyboardInsetListener { keyboardVisible ->
|
||||||
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
|
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
|
||||||
Log.i(
|
Log.i(
|
||||||
"[Main Activity] Keyboard is ${if (keyboardVisible) "visible" else "invisible"}, orientation is ${if (portraitOrientation) "portrait" else "landscape"}"
|
"[Main Activity] Keyboard is ${if (keyboardVisible) "visible" else "invisible"}, orientation is ${if (portraitOrientation) "portrait" else "landscape"}"
|
||||||
)
|
)
|
||||||
shouldTabsBeVisibleDueToOrientationAndKeyboard = !portraitOrientation || !keyboardVisible
|
shouldTabsBeVisibleDueToOrientationAndKeyboard = !portraitOrientation || !keyboardVisible
|
||||||
updateTabsFragmentVisibility()
|
updateTabsFragmentVisibility()
|
||||||
|
|
||||||
|
for (listener in keyboardVisibilityListeners) {
|
||||||
|
listener.onKeyboardVisibilityChanged(keyboardVisible)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initOverlay()
|
initOverlay()
|
||||||
|
@ -246,6 +284,14 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
updateTabsFragmentVisibility()
|
updateTabsFragmentVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addKeyboardVisibilityListener(listener: AppUtils.KeyboardVisibilityListener) {
|
||||||
|
keyboardVisibilityListeners.add(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeKeyboardVisibilityListener(listener: AppUtils.KeyboardVisibilityListener) {
|
||||||
|
keyboardVisibilityListeners.remove(listener)
|
||||||
|
}
|
||||||
|
|
||||||
fun hideKeyboard() {
|
fun hideKeyboard() {
|
||||||
currentFocus?.hideKeyboard()
|
currentFocus?.hideKeyboard()
|
||||||
}
|
}
|
||||||
|
@ -343,7 +389,12 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
// Prevent this intent to be processed again
|
// Prevent this intent to be processed again
|
||||||
intent.action = null
|
intent.action = null
|
||||||
intent.data = null
|
intent.data = null
|
||||||
intent.extras?.clear()
|
val extras = intent.extras
|
||||||
|
if (extras != null) {
|
||||||
|
for (key in extras.keySet()) {
|
||||||
|
intent.removeExtra(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleMainIntent(intent: Intent) {
|
private fun handleMainIntent(intent: Intent) {
|
||||||
|
@ -420,6 +471,8 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addressToCall = addressToCall.replace("%40", "@")
|
||||||
|
|
||||||
val address = coreContext.core.interpretUrl(
|
val address = coreContext.core.interpretUrl(
|
||||||
addressToCall,
|
addressToCall,
|
||||||
LinphoneUtils.applyInternationalPrefix()
|
LinphoneUtils.applyInternationalPrefix()
|
||||||
|
@ -630,4 +683,43 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
|
|
||||||
dialog.show()
|
dialog.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showAuthenticationRequestedDialog(
|
||||||
|
authInfo: AuthInfo
|
||||||
|
) {
|
||||||
|
val identity = "${authInfo.username}@${authInfo.domain}"
|
||||||
|
Log.i("[Main Activity] Showing authentication required dialog for account [$identity]")
|
||||||
|
|
||||||
|
val dialogViewModel = DialogViewModel(
|
||||||
|
getString(R.string.dialog_authentication_required_message, identity),
|
||||||
|
getString(R.string.dialog_authentication_required_title)
|
||||||
|
)
|
||||||
|
dialogViewModel.showPassword = true
|
||||||
|
dialogViewModel.passwordTitle = getString(
|
||||||
|
R.string.settings_password_protection_dialog_input_hint
|
||||||
|
)
|
||||||
|
val dialog = DialogUtils.getDialog(this, dialogViewModel)
|
||||||
|
|
||||||
|
dialogViewModel.showCancelButton {
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogViewModel.showOkButton(
|
||||||
|
{
|
||||||
|
Log.i(
|
||||||
|
"[Main Activity] Updating password for account [$identity] using auth info [$authInfo]"
|
||||||
|
)
|
||||||
|
val newPassword = dialogViewModel.password
|
||||||
|
authInfo.password = newPassword
|
||||||
|
coreContext.core.addAuthInfo(authInfo)
|
||||||
|
|
||||||
|
coreContext.core.refreshRegisters()
|
||||||
|
|
||||||
|
dialog.dismiss()
|
||||||
|
},
|
||||||
|
getString(R.string.dialog_authentication_required_change_password_label)
|
||||||
|
)
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,10 +35,11 @@ class ChatMessageAttachmentData(
|
||||||
init {
|
init {
|
||||||
val extension = FileUtils.getExtensionFromFileName(path)
|
val extension = FileUtils.getExtensionFromFileName(path)
|
||||||
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||||
isImage = FileUtils.isMimeImage(mime)
|
val mimeType = FileUtils.getMimeType(mime)
|
||||||
isVideo = FileUtils.isMimeVideo(mime)
|
isImage = mimeType == FileUtils.MimeType.Image
|
||||||
isAudio = FileUtils.isMimeAudio(mime)
|
isVideo = mimeType == FileUtils.MimeType.Video
|
||||||
isPdf = FileUtils.isMimePdf(mime)
|
isAudio = mimeType == FileUtils.MimeType.Audio
|
||||||
|
isPdf = mimeType == FileUtils.MimeType.Pdf
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete() {
|
fun delete() {
|
||||||
|
|
|
@ -83,7 +83,6 @@ class ChatMessageContentData(
|
||||||
val conferenceDate = MutableLiveData<String>()
|
val conferenceDate = MutableLiveData<String>()
|
||||||
val conferenceTime = MutableLiveData<String>()
|
val conferenceTime = MutableLiveData<String>()
|
||||||
val conferenceDuration = MutableLiveData<String>()
|
val conferenceDuration = MutableLiveData<String>()
|
||||||
var conferenceAddress = MutableLiveData<String>()
|
|
||||||
val showDuration = MutableLiveData<Boolean>()
|
val showDuration = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
val isAlone: Boolean
|
val isAlone: Boolean
|
||||||
|
@ -107,6 +106,8 @@ class ChatMessageContentData(
|
||||||
stopVoiceRecording()
|
stopVoiceRecording()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var conferenceAddress: String? = null
|
||||||
|
|
||||||
private fun getContent(): Content {
|
private fun getContent(): Content {
|
||||||
return chatMessage.contents[contentIndex]
|
return chatMessage.contents[contentIndex]
|
||||||
}
|
}
|
||||||
|
@ -182,7 +183,7 @@ class ChatMessageContentData(
|
||||||
val content = getContent()
|
val content = getContent()
|
||||||
val filePath = content.filePath
|
val filePath = content.filePath
|
||||||
if (content.isFileTransfer) {
|
if (content.isFileTransfer) {
|
||||||
if (filePath == null || filePath.isEmpty()) {
|
if (filePath.isNullOrEmpty()) {
|
||||||
val contentName = content.name
|
val contentName = content.name
|
||||||
if (contentName != null) {
|
if (contentName != null) {
|
||||||
val file = FileUtils.getFileStoragePath(contentName)
|
val file = FileUtils.getFileStoragePath(contentName)
|
||||||
|
@ -274,18 +275,26 @@ class ChatMessageContentData(
|
||||||
filePath.value = path
|
filePath.value = path
|
||||||
val extension = FileUtils.getExtensionFromFileName(path)
|
val extension = FileUtils.getExtensionFromFileName(path)
|
||||||
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||||
isImage.value = FileUtils.isMimeImage(mime)
|
val type = when (FileUtils.getMimeType(mime)) {
|
||||||
isVideo.value = FileUtils.isMimeVideo(mime) && !isVoiceRecord
|
FileUtils.MimeType.Image -> {
|
||||||
isAudio.value = FileUtils.isMimeAudio(mime) && !isVoiceRecord
|
isImage.value = true
|
||||||
isPdf.value = FileUtils.isMimePdf(mime)
|
"image"
|
||||||
val type = when {
|
}
|
||||||
isImage.value == true -> "image"
|
FileUtils.MimeType.Video -> {
|
||||||
isVideo.value == true -> "video"
|
isVideo.value = !isVoiceRecord
|
||||||
isAudio.value == true -> "audio"
|
if (isVoiceRecord) "voice recording" else "video"
|
||||||
isPdf.value == true -> "pdf"
|
}
|
||||||
isVoiceRecord -> "voice recording"
|
FileUtils.MimeType.Audio -> {
|
||||||
isConferenceIcs -> "conference invitation"
|
isAudio.value = !isVoiceRecord
|
||||||
else -> "unknown"
|
if (isVoiceRecord) "voice recording" else "audio"
|
||||||
|
}
|
||||||
|
FileUtils.MimeType.Pdf -> {
|
||||||
|
isPdf.value = true
|
||||||
|
"pdf"
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (isConferenceIcs) "conference invitation" else "unknown"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Log.i(
|
Log.i(
|
||||||
"[Content] Extension for file [$path] is [$extension], deduced type from MIME is [$type]"
|
"[Content] Extension for file [$path] is [$extension], deduced type from MIME is [$type]"
|
||||||
|
@ -310,23 +319,26 @@ class ChatMessageContentData(
|
||||||
Log.w(
|
Log.w(
|
||||||
"[Content] Found ${if (content.isFile) "file" else "file transfer"} content with empty path..."
|
"[Content] Found ${if (content.isFile) "file" else "file transfer"} content with empty path..."
|
||||||
)
|
)
|
||||||
isImage.value = false
|
|
||||||
isVideo.value = false
|
|
||||||
isAudio.value = false
|
|
||||||
isPdf.value = false
|
|
||||||
isVoiceRecording.value = false
|
|
||||||
isConferenceSchedule.value = false
|
|
||||||
}
|
}
|
||||||
} else if (content.isFileTransfer) {
|
} else if (content.isFileTransfer) {
|
||||||
downloadable.value = true
|
downloadable.value = true
|
||||||
val extension = FileUtils.getExtensionFromFileName(fileName.value!!)
|
val extension = FileUtils.getExtensionFromFileName(fileName.value!!)
|
||||||
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||||
isImage.value = FileUtils.isMimeImage(mime)
|
when (FileUtils.getMimeType(mime)) {
|
||||||
isVideo.value = FileUtils.isMimeVideo(mime)
|
FileUtils.MimeType.Image -> {
|
||||||
isAudio.value = FileUtils.isMimeAudio(mime)
|
isImage.value = true
|
||||||
isPdf.value = FileUtils.isMimePdf(mime)
|
}
|
||||||
isVoiceRecording.value = false
|
FileUtils.MimeType.Video -> {
|
||||||
isConferenceSchedule.value = false
|
isVideo.value = true
|
||||||
|
}
|
||||||
|
FileUtils.MimeType.Audio -> {
|
||||||
|
isAudio.value = true
|
||||||
|
}
|
||||||
|
FileUtils.MimeType.Pdf -> {
|
||||||
|
isPdf.value = true
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
} else if (content.isIcalendar) {
|
} else if (content.isIcalendar) {
|
||||||
Log.i("[Content] Found content with icalendar body")
|
Log.i("[Content] Found content with icalendar body")
|
||||||
isConferenceSchedule.value = true
|
isConferenceSchedule.value = true
|
||||||
|
@ -342,9 +354,9 @@ class ChatMessageContentData(
|
||||||
val conferenceInfo = Factory.instance().createConferenceInfoFromIcalendarContent(content)
|
val conferenceInfo = Factory.instance().createConferenceInfoFromIcalendarContent(content)
|
||||||
val conferenceUri = conferenceInfo?.uri?.asStringUriOnly()
|
val conferenceUri = conferenceInfo?.uri?.asStringUriOnly()
|
||||||
if (conferenceInfo != null && conferenceUri != null) {
|
if (conferenceInfo != null && conferenceUri != null) {
|
||||||
conferenceAddress.value = conferenceUri!!
|
conferenceAddress = conferenceUri!!
|
||||||
Log.i(
|
Log.i(
|
||||||
"[Content] Created conference info from ICS with address ${conferenceAddress.value}"
|
"[Content] Created conference info from ICS with address $conferenceAddress"
|
||||||
)
|
)
|
||||||
conferenceSubject.value = conferenceInfo.subject
|
conferenceSubject.value = conferenceInfo.subject
|
||||||
conferenceDescription.value = conferenceInfo.description
|
conferenceDescription.value = conferenceInfo.description
|
||||||
|
@ -407,7 +419,7 @@ class ChatMessageContentData(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun callConferenceAddress() {
|
fun callConferenceAddress() {
|
||||||
val address = conferenceAddress.value
|
val address = conferenceAddress
|
||||||
if (address == null) {
|
if (address == null) {
|
||||||
Log.e("[Content] Can't call null conference address!")
|
Log.e("[Content] Can't call null conference address!")
|
||||||
return
|
return
|
||||||
|
|
|
@ -120,6 +120,17 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val keyboardVisibilityListener = object : AppUtils.KeyboardVisibilityListener {
|
||||||
|
override fun onKeyboardVisibilityChanged(visible: Boolean) {
|
||||||
|
if (visible && chatSendingViewModel.isEmojiPickerOpen.value == true) {
|
||||||
|
Log.d(
|
||||||
|
"[Chat Room] Emoji picker is opened, closing it because keyboard is now visible"
|
||||||
|
)
|
||||||
|
chatSendingViewModel.isEmojiPickerOpen.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var chatScrollListener: ChatScrollListener
|
private lateinit var chatScrollListener: ChatScrollListener
|
||||||
|
|
||||||
override fun getLayoutId(): Int {
|
override fun getLayoutId(): Int {
|
||||||
|
@ -202,15 +213,6 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
sharedViewModel.chatRoomFragmentOpenedEvent.value = Event(true)
|
sharedViewModel.chatRoomFragmentOpenedEvent.value = Event(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.root.addKeyboardInsetListener { keyboardVisible ->
|
|
||||||
if (keyboardVisible && chatSendingViewModel.isEmojiPickerOpen.value == true) {
|
|
||||||
Log.d(
|
|
||||||
"[Chat Room] Emoji picker is opened, closing it because keyboard is now visible"
|
|
||||||
)
|
|
||||||
chatSendingViewModel.isEmojiPickerOpen.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Compatibility.setLocusIdInContentCaptureSession(binding.root, chatRoom)
|
Compatibility.setLocusIdInContentCaptureSession(binding.root, chatRoom)
|
||||||
|
|
||||||
isSecure = chatRoom.currentParams.isEncryptionEnabled
|
isSecure = chatRoom.currentParams.isEncryptionEnabled
|
||||||
|
@ -469,7 +471,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
} else {
|
} else {
|
||||||
if (path.isEmpty()) {
|
if (path.isEmpty()) {
|
||||||
val name = content.name
|
val name = content.name
|
||||||
if (name != null && name.isNotEmpty()) {
|
if (!name.isNullOrEmpty()) {
|
||||||
val file = FileUtils.getFileStoragePath(name)
|
val file = FileUtils.getFileStoragePath(name)
|
||||||
FileUtils.writeIntoFile(content.buffer, file)
|
FileUtils.writeIntoFile(content.buffer, file)
|
||||||
path = file.absolutePath
|
path = file.absolutePath
|
||||||
|
@ -478,8 +480,8 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
"[Chat Room] Content file path was empty, created file from buffer at $path"
|
"[Chat Room] Content file path was empty, created file from buffer at $path"
|
||||||
)
|
)
|
||||||
} else if (content.isIcalendar) {
|
} else if (content.isIcalendar) {
|
||||||
val name = "conference.ics"
|
val filename = "conference.ics"
|
||||||
val file = FileUtils.getFileStoragePath(name)
|
val file = FileUtils.getFileStoragePath(filename)
|
||||||
FileUtils.writeIntoFile(content.buffer, file)
|
FileUtils.writeIntoFile(content.buffer, file)
|
||||||
path = file.absolutePath
|
path = file.absolutePath
|
||||||
content.filePath = path
|
content.filePath = path
|
||||||
|
@ -498,20 +500,20 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
|
|
||||||
val extension = FileUtils.getExtensionFromFileName(path)
|
val extension = FileUtils.getExtensionFromFileName(path)
|
||||||
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||||
when {
|
when (FileUtils.getMimeType(mime)) {
|
||||||
FileUtils.isMimeImage(mime) -> navigateToImageFileViewer(
|
FileUtils.MimeType.Image -> navigateToImageFileViewer(
|
||||||
preventScreenshots
|
preventScreenshots
|
||||||
)
|
)
|
||||||
FileUtils.isMimeVideo(mime) -> navigateToVideoFileViewer(
|
FileUtils.MimeType.Video -> navigateToVideoFileViewer(
|
||||||
preventScreenshots
|
preventScreenshots
|
||||||
)
|
)
|
||||||
FileUtils.isMimeAudio(mime) -> navigateToAudioFileViewer(
|
FileUtils.MimeType.Audio -> navigateToAudioFileViewer(
|
||||||
preventScreenshots
|
preventScreenshots
|
||||||
)
|
)
|
||||||
FileUtils.isMimePdf(mime) -> navigateToPdfFileViewer(
|
FileUtils.MimeType.Pdf -> navigateToPdfFileViewer(
|
||||||
preventScreenshots
|
preventScreenshots
|
||||||
)
|
)
|
||||||
FileUtils.isMimePlainText(mime) -> navigateToTextFileViewer(
|
FileUtils.MimeType.PlainText -> navigateToTextFileViewer(
|
||||||
preventScreenshots
|
preventScreenshots
|
||||||
)
|
)
|
||||||
else -> {
|
else -> {
|
||||||
|
@ -584,9 +586,9 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
viewLifecycleOwner
|
viewLifecycleOwner
|
||||||
) {
|
) {
|
||||||
it.consume { chatMessage ->
|
it.consume { chatMessage ->
|
||||||
var index = 0
|
var index: Int
|
||||||
var retryCount = 0
|
var loadSteps = 0
|
||||||
var expectedChildCount = 0
|
var expectedChildCount: Int
|
||||||
do {
|
do {
|
||||||
val events = listViewModel.events.value.orEmpty()
|
val events = listViewModel.events.value.orEmpty()
|
||||||
expectedChildCount = events.size
|
expectedChildCount = events.size
|
||||||
|
@ -600,18 +602,17 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
}
|
}
|
||||||
index = events.indexOf(eventLog)
|
index = events.indexOf(eventLog)
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
retryCount += 1
|
loadSteps += 1
|
||||||
listViewModel.loadMoreData(events.size)
|
listViewModel.loadMoreData(events.size)
|
||||||
}
|
}
|
||||||
} while (index == -1 && retryCount < 5)
|
} while (index == -1 && loadSteps < 5)
|
||||||
|
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
if (retryCount == 0) {
|
if (loadSteps == 0) {
|
||||||
scrollTo(index, true)
|
scrollTo(index, true)
|
||||||
} else {
|
} else {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
val layoutManager = binding.chatMessagesList.layoutManager as LinearLayoutManager
|
|
||||||
var retryCount = 0
|
var retryCount = 0
|
||||||
do {
|
do {
|
||||||
// We have to wait for newly loaded items to be added to list before being able to scroll
|
// We have to wait for newly loaded items to be added to list before being able to scroll
|
||||||
|
@ -696,7 +697,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.setVoiceRecordingTouchListener { view, event ->
|
binding.setVoiceRecordingTouchListener { _, event ->
|
||||||
if (corePreferences.holdToRecordVoiceMessage) {
|
if (corePreferences.holdToRecordVoiceMessage) {
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
MotionEvent.ACTION_DOWN -> {
|
MotionEvent.ACTION_DOWN -> {
|
||||||
|
@ -846,7 +847,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
if (_adapter != null) {
|
if (_adapter != null) {
|
||||||
try {
|
try {
|
||||||
adapter.registerAdapterDataObserver(observer)
|
adapter.registerAdapterDataObserver(observer)
|
||||||
} catch (ise: IllegalStateException) {}
|
} catch (_: IllegalStateException) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for items to be displayed
|
// Wait for items to be displayed
|
||||||
|
@ -858,6 +859,10 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
"[Chat Room] Fragment resuming but viewModel lateinit property isn't initialized!"
|
"[Chat Room] Fragment resuming but viewModel lateinit property isn't initialized!"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(requireActivity() as MainActivity).addKeyboardVisibilityListener(
|
||||||
|
keyboardVisibilityListener
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
@ -871,12 +876,16 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
if (_adapter != null) {
|
if (_adapter != null) {
|
||||||
try {
|
try {
|
||||||
adapter.unregisterAdapterDataObserver(observer)
|
adapter.unregisterAdapterDataObserver(observer)
|
||||||
} catch (ise: IllegalStateException) {}
|
} catch (_: IllegalStateException) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conversation isn't visible anymore, any new message received in it will trigger a notification
|
// Conversation isn't visible anymore, any new message received in it will trigger a notification
|
||||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
||||||
|
|
||||||
|
(requireActivity() as MainActivity).removeKeyboardVisibilityListener(
|
||||||
|
keyboardVisibilityListener
|
||||||
|
)
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -107,6 +107,8 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
|
||||||
|
|
||||||
val isEmojiPickerVisible = MutableLiveData<Boolean>()
|
val isEmojiPickerVisible = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val isFileTransferAvailable = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
val requestKeyboardHidingEvent: MutableLiveData<Event<Boolean>> by lazy {
|
val requestKeyboardHidingEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||||
MutableLiveData<Event<Boolean>>()
|
MutableLiveData<Event<Boolean>>()
|
||||||
}
|
}
|
||||||
|
@ -146,6 +148,7 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
|
||||||
sendMessageEnabled.value = false
|
sendMessageEnabled.value = false
|
||||||
isEmojiPickerOpen.value = false
|
isEmojiPickerOpen.value = false
|
||||||
isEmojiPickerVisible.value = corePreferences.showEmojiPickerButton
|
isEmojiPickerVisible.value = corePreferences.showEmojiPickerButton
|
||||||
|
isFileTransferAvailable.value = LinphoneUtils.isFileTransferAvailable()
|
||||||
updateChatRoomReadOnlyState()
|
updateChatRoomReadOnlyState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,6 +177,10 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
|
||||||
|
|
||||||
fun onTextToSendChanged(value: String) {
|
fun onTextToSendChanged(value: String) {
|
||||||
sendMessageEnabled.value = value.trim().isNotEmpty() || attachments.value?.isNotEmpty() == true || isPendingVoiceRecord.value == true
|
sendMessageEnabled.value = value.trim().isNotEmpty() || attachments.value?.isNotEmpty() == true || isPendingVoiceRecord.value == true
|
||||||
|
|
||||||
|
val showEmojiPicker = value.isEmpty() || AppUtils.isTextOnlyContainingEmoji(value)
|
||||||
|
isEmojiPickerVisible.value = corePreferences.showEmojiPickerButton && showEmojiPicker
|
||||||
|
|
||||||
if (value.isNotEmpty()) {
|
if (value.isNotEmpty()) {
|
||||||
if (attachFileEnabled.value == true && !corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
if (attachFileEnabled.value == true && !corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||||
attachFileEnabled.value = false
|
attachFileEnabled.value = false
|
||||||
|
|
|
@ -25,6 +25,8 @@ import android.text.method.LinkMovementMethod
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import androidx.appcompat.widget.AppCompatTextView
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.round
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The purpose of this class is to have a TextView declared with wrap_content as width that won't
|
* The purpose of this class is to have a TextView declared with wrap_content as width that won't
|
||||||
|
@ -52,10 +54,12 @@ class MultiLineWrapContentWidthTextView : AppCompatTextView {
|
||||||
|
|
||||||
if (layout != null && layout.lineCount >= 2) {
|
if (layout != null && layout.lineCount >= 2) {
|
||||||
val maxLineWidth = ceil(getMaxLineWidth(layout)).toInt()
|
val maxLineWidth = ceil(getMaxLineWidth(layout)).toInt()
|
||||||
val uselessPaddingWidth = layout.width - maxLineWidth
|
if (maxLineWidth < measuredWidth) {
|
||||||
val width = measuredWidth - uselessPaddingWidth
|
super.onMeasure(
|
||||||
val height = measuredHeight
|
MeasureSpec.makeMeasureSpec(maxLineWidth, MeasureSpec.getMode(widthSpec)),
|
||||||
setMeasuredDimension(width, height)
|
heightSpec
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,10 +67,8 @@ class MultiLineWrapContentWidthTextView : AppCompatTextView {
|
||||||
var maxWidth = 0.0f
|
var maxWidth = 0.0f
|
||||||
val lines = layout.lineCount
|
val lines = layout.lineCount
|
||||||
for (i in 0 until lines) {
|
for (i in 0 until lines) {
|
||||||
if (layout.getLineWidth(i) > maxWidth) {
|
maxWidth = max(maxWidth, layout.getLineWidth(i))
|
||||||
maxWidth = layout.getLineWidth(i)
|
|
||||||
}
|
}
|
||||||
}
|
return round(maxWidth)
|
||||||
return maxWidth
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,13 +89,26 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
|
||||||
|
|
||||||
address.value = conferenceAddress!!
|
address.value = conferenceAddress!!
|
||||||
|
|
||||||
if (scheduleForLater.value == true && sendInviteViaChat.value == true) {
|
if (scheduleForLater.value == true) {
|
||||||
|
if (sendInviteViaChat.value == true) {
|
||||||
// Send conference info even when conf is not scheduled for later
|
// Send conference info even when conf is not scheduled for later
|
||||||
// as the conference server doesn't invite participants automatically
|
// as the conference server doesn't invite participants automatically
|
||||||
|
Log.i(
|
||||||
|
"[Conference Creation] Scheduled conference is ready, sending invitations by chat"
|
||||||
|
)
|
||||||
val chatRoomParams = LinphoneUtils.getConferenceInvitationsChatRoomParams()
|
val chatRoomParams = LinphoneUtils.getConferenceInvitationsChatRoomParams()
|
||||||
conferenceScheduler.sendInvitations(chatRoomParams)
|
conferenceScheduler.sendInvitations(chatRoomParams)
|
||||||
} else {
|
} else {
|
||||||
// Will be done in coreListener
|
Log.i(
|
||||||
|
"[Conference Creation] Scheduled conference is ready, we were asked not to send invitations by chat so leaving fragment"
|
||||||
|
)
|
||||||
|
conferenceCreationInProgress.value = false
|
||||||
|
conferenceCreationCompletedEvent.value = Event(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.i("[Conference Creation] Group call is ready, leaving fragment")
|
||||||
|
conferenceCreationInProgress.value = false
|
||||||
|
conferenceCreationCompletedEvent.value = Event(true)
|
||||||
}
|
}
|
||||||
} else if (state == ConferenceScheduler.State.Error) {
|
} else if (state == ConferenceScheduler.State.Error) {
|
||||||
Log.e("[Conference Creation] Failed to create conference!")
|
Log.e("[Conference Creation] Failed to create conference!")
|
||||||
|
@ -134,30 +147,6 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val coreListener: CoreListenerStub = object : CoreListenerStub() {
|
|
||||||
override fun onCallStateChanged(
|
|
||||||
core: Core,
|
|
||||||
call: Call,
|
|
||||||
state: Call.State?,
|
|
||||||
message: String
|
|
||||||
) {
|
|
||||||
when (state) {
|
|
||||||
Call.State.OutgoingProgress -> {
|
|
||||||
conferenceCreationInProgress.value = false
|
|
||||||
}
|
|
||||||
Call.State.End -> {
|
|
||||||
Log.i("[Conference Creation] Call has ended, leaving waiting room fragment")
|
|
||||||
conferenceCreationCompletedEvent.value = Event(true)
|
|
||||||
}
|
|
||||||
Call.State.Error -> {
|
|
||||||
Log.w("[Conference Creation] Call has failed, leaving waiting room fragment")
|
|
||||||
conferenceCreationCompletedEvent.value = Event(true)
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
sipContactsSelected.value = true
|
sipContactsSelected.value = true
|
||||||
|
|
||||||
|
@ -191,11 +180,9 @@ class ConferenceSchedulingViewModel : ContactsSelectionViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
conferenceScheduler.addListener(listener)
|
conferenceScheduler.addListener(listener)
|
||||||
coreContext.core.addListener(coreListener)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
coreContext.core.removeListener(coreListener)
|
|
||||||
conferenceScheduler.removeListener(listener)
|
conferenceScheduler.removeListener(listener)
|
||||||
participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy)
|
participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy)
|
||||||
|
|
||||||
|
|
|
@ -398,13 +398,15 @@ class ConferenceWaitingRoomViewModel : MessageNotifierViewModel() {
|
||||||
|
|
||||||
private fun onAudioDevicesListUpdated() {
|
private fun onAudioDevicesListUpdated() {
|
||||||
val bluetoothDeviceAvailable = AudioRouteUtils.isBluetoothAudioRouteAvailable()
|
val bluetoothDeviceAvailable = AudioRouteUtils.isBluetoothAudioRouteAvailable()
|
||||||
|
if (!bluetoothDeviceAvailable && audioRoutesEnabled.value == true) {
|
||||||
|
Log.w(
|
||||||
|
"[Conference Waiting Room] Bluetooth device no longer available, switching back to default microphone & earpiece/speaker"
|
||||||
|
)
|
||||||
|
}
|
||||||
audioRoutesEnabled.value = bluetoothDeviceAvailable
|
audioRoutesEnabled.value = bluetoothDeviceAvailable
|
||||||
|
|
||||||
if (!bluetoothDeviceAvailable) {
|
if (!bluetoothDeviceAvailable) {
|
||||||
audioRoutesSelected.value = false
|
audioRoutesSelected.value = false
|
||||||
Log.w(
|
|
||||||
"[Conference Waiting Room] Bluetooth device no longer available, switching back to default microphone & earpiece/speaker"
|
|
||||||
)
|
|
||||||
if (isBluetoothHeadsetSelected.value == true) {
|
if (isBluetoothHeadsetSelected.value == true) {
|
||||||
for (audioDevice in coreContext.core.audioDevices) {
|
for (audioDevice in coreContext.core.audioDevices) {
|
||||||
if (isVideoEnabled.value == true) {
|
if (isVideoEnabled.value == true) {
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
package org.linphone.activities.main.contact.fragments
|
package org.linphone.activities.main.contact.fragments
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.Dialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
@ -38,9 +39,11 @@ import org.linphone.activities.main.MainActivity
|
||||||
import org.linphone.activities.main.contact.data.ContactEditorData
|
import org.linphone.activities.main.contact.data.ContactEditorData
|
||||||
import org.linphone.activities.main.contact.data.NumberOrAddressEditorData
|
import org.linphone.activities.main.contact.data.NumberOrAddressEditorData
|
||||||
import org.linphone.activities.main.contact.viewmodels.*
|
import org.linphone.activities.main.contact.viewmodels.*
|
||||||
|
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||||
import org.linphone.activities.navigateToContact
|
import org.linphone.activities.navigateToContact
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.databinding.ContactEditorFragmentBinding
|
import org.linphone.databinding.ContactEditorFragmentBinding
|
||||||
|
import org.linphone.utils.DialogUtils
|
||||||
import org.linphone.utils.FileUtils
|
import org.linphone.utils.FileUtils
|
||||||
import org.linphone.utils.PermissionHelper
|
import org.linphone.utils.PermissionHelper
|
||||||
|
|
||||||
|
@ -71,10 +74,38 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
|
||||||
data.syncAccountName = null
|
data.syncAccountName = null
|
||||||
data.syncAccountType = null
|
data.syncAccountType = null
|
||||||
|
|
||||||
if (data.friend == null && corePreferences.showNewContactAccountDialog) {
|
if (data.friend == null) {
|
||||||
|
var atLeastASipAddressOrPhoneNumber = false
|
||||||
|
for (addr in data.addresses.value.orEmpty()) {
|
||||||
|
if (addr.newValue.value.orEmpty().isNotEmpty()) {
|
||||||
|
atLeastASipAddressOrPhoneNumber = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!atLeastASipAddressOrPhoneNumber) {
|
||||||
|
for (number in data.numbers.value.orEmpty()) {
|
||||||
|
if (number.newValue.value.orEmpty().isNotEmpty()) {
|
||||||
|
atLeastASipAddressOrPhoneNumber = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!atLeastASipAddressOrPhoneNumber) {
|
||||||
|
// Contact will be created without phone and SIP address
|
||||||
|
// Let's warn the user it won't be visible in Linphone app
|
||||||
|
Log.w(
|
||||||
|
"[Contact Editor] New contact without SIP address nor phone number, showing warning dialog"
|
||||||
|
)
|
||||||
|
showInvisibleContactWarningDialog()
|
||||||
|
} else if (corePreferences.showNewContactAccountDialog) {
|
||||||
Log.i("[Contact Editor] New contact, ask user where to store it")
|
Log.i("[Contact Editor] New contact, ask user where to store it")
|
||||||
SyncAccountPickerFragment(this).show(childFragmentManager, "SyncAccountPicker")
|
SyncAccountPickerFragment(this).show(childFragmentManager, "SyncAccountPicker")
|
||||||
} else {
|
} else {
|
||||||
|
Log.i("[Contact Editor] Saving new contact")
|
||||||
|
saveContact()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.i("[Contact Editor] Saving contact changes")
|
||||||
saveContact()
|
saveContact()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -98,7 +129,7 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSyncAccountClicked(name: String?, type: String?) {
|
override fun onSyncAccountClicked(name: String?, type: String?) {
|
||||||
Log.i("[Contact Editor] Using account $name / $type")
|
Log.i("[Contact Editor] Saving new contact using account $name / $type")
|
||||||
data.syncAccountName = name
|
data.syncAccountName = name
|
||||||
data.syncAccountType = type
|
data.syncAccountType = type
|
||||||
saveContact()
|
saveContact()
|
||||||
|
@ -146,6 +177,9 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
|
||||||
Log.i("[Contact Editor] Displaying contact $savedContact")
|
Log.i("[Contact Editor] Displaying contact $savedContact")
|
||||||
navigateToContact(id)
|
navigateToContact(id)
|
||||||
} else {
|
} else {
|
||||||
|
Log.w(
|
||||||
|
"[Contact Editor] Can't display $savedContact because it doesn't have a refKey, going back"
|
||||||
|
)
|
||||||
goBack()
|
goBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -183,4 +217,35 @@ class ContactEditorFragment : GenericFragment<ContactEditorFragmentBinding>(), S
|
||||||
|
|
||||||
startActivityForResult(chooserIntent, 0)
|
startActivityForResult(chooserIntent, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showInvisibleContactWarningDialog() {
|
||||||
|
val dialogViewModel =
|
||||||
|
DialogViewModel(getString(R.string.contacts_new_contact_wont_be_visible_warning_dialog))
|
||||||
|
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||||
|
|
||||||
|
dialogViewModel.showCancelButton(
|
||||||
|
{
|
||||||
|
Log.i("[Contact Editor] Aborting new contact saving")
|
||||||
|
dialog.dismiss()
|
||||||
|
},
|
||||||
|
getString(R.string.no)
|
||||||
|
)
|
||||||
|
|
||||||
|
dialogViewModel.showOkButton(
|
||||||
|
{
|
||||||
|
dialog.dismiss()
|
||||||
|
|
||||||
|
if (corePreferences.showNewContactAccountDialog) {
|
||||||
|
Log.i("[Contact Editor] New contact, ask user where to store it")
|
||||||
|
SyncAccountPickerFragment(this).show(childFragmentManager, "SyncAccountPicker")
|
||||||
|
} else {
|
||||||
|
Log.i("[Contact Editor] Saving new contact")
|
||||||
|
saveContact()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getString(R.string.yes)
|
||||||
|
)
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ import org.linphone.activities.main.contact.data.ContactNumberOrAddressData
|
||||||
import org.linphone.activities.main.viewmodels.MessageNotifierViewModel
|
import org.linphone.activities.main.viewmodels.MessageNotifierViewModel
|
||||||
import org.linphone.contact.ContactDataInterface
|
import org.linphone.contact.ContactDataInterface
|
||||||
import org.linphone.contact.ContactsUpdatedListenerStub
|
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||||
import org.linphone.contact.hasPresence
|
import org.linphone.contact.hasLongTermPresence
|
||||||
import org.linphone.core.*
|
import org.linphone.core.*
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.utils.Event
|
import org.linphone.utils.Event
|
||||||
|
@ -81,6 +81,8 @@ class ContactViewModel(friend: Friend, async: Boolean = false) : MessageNotifier
|
||||||
|
|
||||||
val readOnlyNativeAddressBook = MutableLiveData<Boolean>()
|
val readOnlyNativeAddressBook = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val hasLongTermPresence = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
private val chatRoomListener = object : ChatRoomListenerStub() {
|
private val chatRoomListener = object : ChatRoomListenerStub() {
|
||||||
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
|
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||||
if (state == ChatRoom.State.Created) {
|
if (state == ChatRoom.State.Created) {
|
||||||
|
@ -147,16 +149,19 @@ class ContactViewModel(friend: Friend, async: Boolean = false) : MessageNotifier
|
||||||
isNativeContact.postValue(friend.refKey != null)
|
isNativeContact.postValue(friend.refKey != null)
|
||||||
presenceStatus.postValue(friend.consolidatedPresence)
|
presenceStatus.postValue(friend.consolidatedPresence)
|
||||||
readOnlyNativeAddressBook.postValue(corePreferences.readOnlyNativeContacts)
|
readOnlyNativeAddressBook.postValue(corePreferences.readOnlyNativeContacts)
|
||||||
|
hasLongTermPresence.postValue(friend.hasLongTermPresence())
|
||||||
} else {
|
} else {
|
||||||
contact.value = friend
|
contact.value = friend
|
||||||
displayName.value = friend.name
|
displayName.value = friend.name
|
||||||
isNativeContact.value = friend.refKey != null
|
isNativeContact.value = friend.refKey != null
|
||||||
presenceStatus.value = friend.consolidatedPresence
|
presenceStatus.value = friend.consolidatedPresence
|
||||||
readOnlyNativeAddressBook.value = corePreferences.readOnlyNativeContacts
|
readOnlyNativeAddressBook.value = corePreferences.readOnlyNativeContacts
|
||||||
|
hasLongTermPresence.value = friend.hasLongTermPresence()
|
||||||
}
|
}
|
||||||
|
|
||||||
friend.addListener {
|
friend.addListener {
|
||||||
presenceStatus.value = it.consolidatedPresence
|
presenceStatus.value = it.consolidatedPresence
|
||||||
|
hasLongTermPresence.value = it.hasLongTermPresence()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,8 +274,4 @@ class ContactViewModel(friend: Friend, async: Boolean = false) : MessageNotifier
|
||||||
}
|
}
|
||||||
numbersAndAddresses.postValue(list)
|
numbersAndAddresses.postValue(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasPresence(): Boolean {
|
|
||||||
return contact.value?.hasPresence() ?: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,17 +119,17 @@ class ContactsListViewModel : ViewModel() {
|
||||||
previousFilter = filterValue
|
previousFilter = filterValue
|
||||||
|
|
||||||
val domain = if (sipContactsSelected.value == true) coreContext.core.defaultAccount?.params?.domain ?: "" else ""
|
val domain = if (sipContactsSelected.value == true) coreContext.core.defaultAccount?.params?.domain ?: "" else ""
|
||||||
val filter = MagicSearch.Source.Friends.toInt() or MagicSearch.Source.LdapServers.toInt()
|
val sources = MagicSearch.Source.Friends.toInt() or MagicSearch.Source.LdapServers.toInt()
|
||||||
val aggregation = MagicSearch.Aggregation.Friend
|
val aggregation = MagicSearch.Aggregation.Friend
|
||||||
searchResultsPending = true
|
searchResultsPending = true
|
||||||
fastFetchJob?.cancel()
|
fastFetchJob?.cancel()
|
||||||
Log.i(
|
Log.i(
|
||||||
"[Contacts] Asking Magic search for contacts matching filter [$filterValue], domain [$domain] and in sources [$filter]"
|
"[Contacts] Asking Magic search for contacts matching filter [$filterValue], domain [$domain] and in sources [$sources]"
|
||||||
)
|
)
|
||||||
coreContext.contactsManager.magicSearch.getContactsListAsync(
|
coreContext.contactsManager.magicSearch.getContactsListAsync(
|
||||||
filterValue,
|
filterValue,
|
||||||
domain,
|
domain,
|
||||||
filter,
|
sources,
|
||||||
aggregation
|
aggregation
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -273,6 +273,24 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
|
||||||
// Don't check the following the previous permissions are being asked
|
// Don't check the following the previous permissions are being asked
|
||||||
checkTelecomManagerPermissions()
|
checkTelecomManagerPermissions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See https://developer.android.com/about/versions/14/behavior-changes-14#fgs-types
|
||||||
|
if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) {
|
||||||
|
val fullScreenIntentPermission = Compatibility.hasFullScreenIntentPermission(
|
||||||
|
requireContext()
|
||||||
|
)
|
||||||
|
Log.i(
|
||||||
|
"[Dialer] Android 14 or above detected: full-screen intent permission is ${if (fullScreenIntentPermission) "granted" else "not granted"}"
|
||||||
|
)
|
||||||
|
if (!fullScreenIntentPermission) {
|
||||||
|
(requireActivity() as MainActivity).showSnackBar(
|
||||||
|
R.string.android_14_full_screen_intent_permission_not_granted,
|
||||||
|
R.string.android_14_go_to_full_screen_intent_permission_setting
|
||||||
|
) {
|
||||||
|
Compatibility.requestFullScreenIntentPermission(requireContext())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Version.API26_O_80)
|
@TargetApi(Version.API26_O_80)
|
||||||
|
|
|
@ -134,7 +134,7 @@ class DialerViewModel : LogsUploadViewModel() {
|
||||||
) {
|
) {
|
||||||
if (result == VersionUpdateCheckResult.NewVersionAvailable) {
|
if (result == VersionUpdateCheckResult.NewVersionAvailable) {
|
||||||
Log.i("[Dialer] Update available, version [$version], url [$url]")
|
Log.i("[Dialer] Update available, version [$version], url [$url]")
|
||||||
if (url != null && url.isNotEmpty()) {
|
if (!url.isNullOrEmpty()) {
|
||||||
updateAvailableEvent.value = Event(url)
|
updateAvailableEvent.value = Event(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,11 @@ class PdfViewerFragment : GenericViewerFragment<FilePdfViewerFragmentBinding>()
|
||||||
)[PdfFileViewModel::class.java]
|
)[PdfFileViewModel::class.java]
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
|
viewModel.rendererReady.observe(viewLifecycleOwner) {
|
||||||
|
it.consume {
|
||||||
adapter = PdfPagesListAdapter(viewModel)
|
adapter = PdfPagesListAdapter(viewModel)
|
||||||
binding.pdfViewPager.adapter = adapter
|
binding.pdfViewPager.adapter = adapter
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,8 +92,8 @@ class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
|
||||||
|
|
||||||
val extension = FileUtils.getExtensionFromFileName(filePath)
|
val extension = FileUtils.getExtensionFromFileName(filePath)
|
||||||
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||||
when {
|
when (FileUtils.getMimeType(mime)) {
|
||||||
FileUtils.isMimeImage(mime) -> {
|
FileUtils.MimeType.Image -> {
|
||||||
val export = lifecycleScope.async {
|
val export = lifecycleScope.async {
|
||||||
Compatibility.addImageToMediaStore(requireContext(), content)
|
Compatibility.addImageToMediaStore(requireContext(), content)
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,7 @@ class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileUtils.isMimeVideo(mime) -> {
|
FileUtils.MimeType.Video -> {
|
||||||
val export = lifecycleScope.async {
|
val export = lifecycleScope.async {
|
||||||
Compatibility.addVideoToMediaStore(requireContext(), content)
|
Compatibility.addVideoToMediaStore(requireContext(), content)
|
||||||
}
|
}
|
||||||
|
@ -123,7 +123,7 @@ class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileUtils.isMimeAudio(mime) -> {
|
FileUtils.MimeType.Audio -> {
|
||||||
val export = lifecycleScope.async {
|
val export = lifecycleScope.async {
|
||||||
Compatibility.addAudioToMediaStore(requireContext(), content)
|
Compatibility.addAudioToMediaStore(requireContext(), content)
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ class AudioFileViewModel(content: Content) : FileViewerViewModel(content), Media
|
||||||
override fun getCurrentPosition(): Int {
|
override fun getCurrentPosition(): Int {
|
||||||
try {
|
try {
|
||||||
return mediaPlayer.currentPosition
|
return mediaPlayer.currentPosition
|
||||||
} catch (ise: IllegalStateException) {}
|
} catch (_: IllegalStateException) {}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,10 +26,15 @@ import android.widget.ImageView
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
import org.linphone.core.Content
|
import org.linphone.core.Content
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
|
import org.linphone.utils.Event
|
||||||
|
|
||||||
class PdfFileViewModelFactory(private val content: Content) :
|
class PdfFileViewModelFactory(private val content: Content) :
|
||||||
ViewModelProvider.NewInstanceFactory() {
|
ViewModelProvider.NewInstanceFactory() {
|
||||||
|
@ -43,31 +48,49 @@ class PdfFileViewModelFactory(private val content: Content) :
|
||||||
class PdfFileViewModel(content: Content) : FileViewerViewModel(content) {
|
class PdfFileViewModel(content: Content) : FileViewerViewModel(content) {
|
||||||
val operationInProgress = MutableLiveData<Boolean>()
|
val operationInProgress = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
private val pdfRenderer: PdfRenderer
|
val rendererReady = MutableLiveData<Event<Boolean>>()
|
||||||
|
|
||||||
|
private lateinit var pdfRenderer: PdfRenderer
|
||||||
|
|
||||||
init {
|
init {
|
||||||
operationInProgress.value = false
|
operationInProgress.value = false
|
||||||
|
|
||||||
val input = ParcelFileDescriptor.open(File(filePath), ParcelFileDescriptor.MODE_READ_ONLY)
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val input = ParcelFileDescriptor.open(
|
||||||
|
File(filePath),
|
||||||
|
ParcelFileDescriptor.MODE_READ_ONLY
|
||||||
|
)
|
||||||
pdfRenderer = PdfRenderer(input)
|
pdfRenderer = PdfRenderer(input)
|
||||||
Log.i("[PDF Viewer] ${pdfRenderer.pageCount} pages in file $filePath")
|
Log.i("[PDF Viewer] ${pdfRenderer.pageCount} pages in file $filePath")
|
||||||
|
rendererReady.postValue(Event(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
if (this::pdfRenderer.isInitialized) {
|
||||||
pdfRenderer.close()
|
pdfRenderer.close()
|
||||||
|
}
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPagesCount(): Int {
|
fun getPagesCount(): Int {
|
||||||
|
if (this::pdfRenderer.isInitialized) {
|
||||||
return pdfRenderer.pageCount
|
return pdfRenderer.pageCount
|
||||||
}
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
fun loadPdfPageInto(index: Int, view: ImageView) {
|
fun loadPdfPageInto(index: Int, view: ImageView) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
operationInProgress.value = true
|
operationInProgress.postValue(true)
|
||||||
|
|
||||||
val page: PdfRenderer.Page = pdfRenderer.openPage(index)
|
val page: PdfRenderer.Page = pdfRenderer.openPage(index)
|
||||||
val width = if (coreContext.screenWidth <= coreContext.screenHeight) coreContext.screenWidth else coreContext.screenHeight
|
val width =
|
||||||
|
if (coreContext.screenWidth <= coreContext.screenHeight) coreContext.screenWidth else coreContext.screenHeight
|
||||||
val bm = Bitmap.createBitmap(
|
val bm = Bitmap.createBitmap(
|
||||||
width.toInt(),
|
width.toInt(),
|
||||||
(width / page.width * page.height).toInt(),
|
(width / page.width * page.height).toInt(),
|
||||||
|
@ -76,12 +99,16 @@ class PdfFileViewModel(content: Content) : FileViewerViewModel(content) {
|
||||||
page.render(bm, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
page.render(bm, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
||||||
page.close()
|
page.close()
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
view.setImageBitmap(bm)
|
view.setImageBitmap(bm)
|
||||||
|
}
|
||||||
|
|
||||||
operationInProgress.value = false
|
operationInProgress.postValue(false)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("[PDF Viewer] Exception: $e")
|
Log.e("[PDF Viewer] Exception: $e")
|
||||||
operationInProgress.value = false
|
operationInProgress.postValue(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,17 +47,10 @@ class TextFileViewModel(content: Content) : FileViewerViewModel(content) {
|
||||||
val text = MutableLiveData<String>()
|
val text = MutableLiveData<String>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
operationInProgress.value = false
|
|
||||||
|
|
||||||
openFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openFile() {
|
|
||||||
operationInProgress.value = true
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
|
operationInProgress.postValue(true)
|
||||||
val br = BufferedReader(FileReader(filePath))
|
val br = BufferedReader(FileReader(filePath))
|
||||||
var line: String?
|
var line: String?
|
||||||
val textBuilder = StringBuilder()
|
val textBuilder = StringBuilder()
|
||||||
|
|
|
@ -50,7 +50,7 @@ abstract class SecureFragment<T : ViewDataBinding> : GenericFragment<T>() {
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
if (isSecure) {
|
if (isSecure) {
|
||||||
enableSecureMode(isSecure)
|
enableSecureMode(true)
|
||||||
} else {
|
} else {
|
||||||
// This is a workaround to prevent a small blink showing the previous secured screen
|
// This is a workaround to prevent a small blink showing the previous secured screen
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
|
|
@ -53,7 +53,9 @@ class DetailCallLogFragment : GenericFragment<HistoryDetailFragmentBinding>() {
|
||||||
|
|
||||||
viewModel = callLogGroup.lastCallLogViewModel
|
viewModel = callLogGroup.lastCallLogViewModel
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
|
if (viewModel.relatedCallLogs.value.orEmpty().isEmpty()) {
|
||||||
viewModel.addRelatedCallLogs(callLogGroup.callLogs)
|
viewModel.addRelatedCallLogs(callLogGroup.callLogs)
|
||||||
|
}
|
||||||
|
|
||||||
useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false
|
useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ class CallLogViewModel(val callLog: CallLog, private val isRelated: Boolean = fa
|
||||||
|
|
||||||
val statusIconResource: Int by lazy {
|
val statusIconResource: Int by lazy {
|
||||||
if (callLog.dir == Call.Dir.Incoming) {
|
if (callLog.dir == Call.Dir.Incoming) {
|
||||||
if (callLog.status == Call.Status.Missed) {
|
if (LinphoneUtils.isCallLogMissed(callLog)) {
|
||||||
R.drawable.call_status_missed
|
R.drawable.call_status_missed
|
||||||
} else {
|
} else {
|
||||||
R.drawable.call_status_incoming
|
R.drawable.call_status_incoming
|
||||||
|
@ -56,7 +56,7 @@ class CallLogViewModel(val callLog: CallLog, private val isRelated: Boolean = fa
|
||||||
|
|
||||||
val iconContentDescription: Int by lazy {
|
val iconContentDescription: Int by lazy {
|
||||||
if (callLog.dir == Call.Dir.Incoming) {
|
if (callLog.dir == Call.Dir.Incoming) {
|
||||||
if (callLog.status == Call.Status.Missed) {
|
if (LinphoneUtils.isCallLogMissed(callLog)) {
|
||||||
R.string.content_description_missed_call
|
R.string.content_description_missed_call
|
||||||
} else {
|
} else {
|
||||||
R.string.content_description_incoming_call
|
R.string.content_description_incoming_call
|
||||||
|
@ -68,7 +68,7 @@ class CallLogViewModel(val callLog: CallLog, private val isRelated: Boolean = fa
|
||||||
|
|
||||||
val directionIconResource: Int by lazy {
|
val directionIconResource: Int by lazy {
|
||||||
if (callLog.dir == Call.Dir.Incoming) {
|
if (callLog.dir == Call.Dir.Incoming) {
|
||||||
if (callLog.status == Call.Status.Missed) {
|
if (LinphoneUtils.isCallLogMissed(callLog)) {
|
||||||
R.drawable.call_missed
|
R.drawable.call_missed
|
||||||
} else {
|
} else {
|
||||||
R.drawable.call_incoming
|
R.drawable.call_incoming
|
||||||
|
|
|
@ -98,14 +98,6 @@ class AccountSettingsFragment : GenericSettingFragment<SettingsAccountFragmentBi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.publishPresenceToggledEvent.observe(
|
|
||||||
viewLifecycleOwner
|
|
||||||
) {
|
|
||||||
it.consume {
|
|
||||||
sharedViewModel.publishPresenceToggled.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.deleteAccountRequiredEvent.observe(
|
viewModel.deleteAccountRequiredEvent.observe(
|
||||||
viewLifecycleOwner
|
viewLifecycleOwner
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -74,6 +74,14 @@ class ContactsSettingsFragment : GenericSettingFragment<SettingsContactsFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.publishPresenceToggledEvent.observe(
|
||||||
|
viewLifecycleOwner
|
||||||
|
) {
|
||||||
|
it.consume {
|
||||||
|
sharedViewModel.publishPresenceToggled.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.ldapNewSettingsListener = object : SettingListenerStub() {
|
viewModel.ldapNewSettingsListener = object : SettingListenerStub() {
|
||||||
override fun onClicked() {
|
override fun onClicked() {
|
||||||
Log.i("[Contacts Settings] Clicked on new LDAP config")
|
Log.i("[Contacts Settings] Clicked on new LDAP config")
|
||||||
|
|
|
@ -75,10 +75,6 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
|
||||||
MutableLiveData<Event<Boolean>>()
|
MutableLiveData<Event<Boolean>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
val publishPresenceToggledEvent: MutableLiveData<Event<Boolean>> by lazy {
|
|
||||||
MutableLiveData<Event<Boolean>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
val displayUsernameInsteadOfIdentity = corePreferences.replaceSipUriByUsername
|
val displayUsernameInsteadOfIdentity = corePreferences.replaceSipUriByUsername
|
||||||
|
|
||||||
private var accountToDelete: Account? = null
|
private var accountToDelete: Account? = null
|
||||||
|
@ -293,7 +289,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
|
||||||
account.params = params
|
account.params = params
|
||||||
transportIndex.value = account.params.transport.toInt()
|
transportIndex.value = account.params.transport.toInt()
|
||||||
} else {
|
} else {
|
||||||
Log.e("[Account Settings] Couldn't parse address: $address")
|
Log.e("[Account Settings] Couldn't parse address: $newValue")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -439,16 +435,6 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
|
||||||
}
|
}
|
||||||
val limeServerUrl = MutableLiveData<String>()
|
val limeServerUrl = MutableLiveData<String>()
|
||||||
|
|
||||||
val publishPresenceListener = object : SettingListenerStub() {
|
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
|
||||||
val params = account.params.clone()
|
|
||||||
params.isPublishEnabled = newValue
|
|
||||||
account.params = params
|
|
||||||
publishPresenceToggledEvent.value = Event(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val publishPresence = MutableLiveData<Boolean>()
|
|
||||||
|
|
||||||
val disableBundleModeListener = object : SettingListenerStub() {
|
val disableBundleModeListener = object : SettingListenerStub() {
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
val params = account.params.clone()
|
val params = account.params.clone()
|
||||||
|
@ -518,7 +504,6 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
|
||||||
limeServerUrl.value = params.limeServerUrl
|
limeServerUrl.value = params.limeServerUrl
|
||||||
|
|
||||||
hideLinkPhoneNumber.value = corePreferences.hideLinkPhoneNumber || params.identityAddress?.domain != corePreferences.defaultDomain
|
hideLinkPhoneNumber.value = corePreferences.hideLinkPhoneNumber || params.identityAddress?.domain != corePreferences.defaultDomain
|
||||||
publishPresence.value = params.isPublishEnabled
|
|
||||||
disableBundleMode.value = !params.isRtpBundleEnabled
|
disableBundleMode.value = !params.isRtpBundleEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -141,7 +141,7 @@ class AudioSettingsViewModel : GenericSettingsViewModel() {
|
||||||
override fun onTextValueChanged(newValue: String) {
|
override fun onTextValueChanged(newValue: String) {
|
||||||
try {
|
try {
|
||||||
core.micGainDb = newValue.toFloat()
|
core.micGainDb = newValue.toFloat()
|
||||||
} catch (nfe: NumberFormatException) {
|
} catch (_: NumberFormatException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -151,7 +151,7 @@ class AudioSettingsViewModel : GenericSettingsViewModel() {
|
||||||
override fun onTextValueChanged(newValue: String) {
|
override fun onTextValueChanged(newValue: String) {
|
||||||
try {
|
try {
|
||||||
core.playbackGainDb = newValue.toFloat()
|
core.playbackGainDb = newValue.toFloat()
|
||||||
} catch (nfe: NumberFormatException) {
|
} catch (_: NumberFormatException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,7 +97,7 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
|
||||||
Log.w("[Call Settings] Disabling Telecom Manager auto-enable")
|
Log.w("[Call Settings] Disabling Telecom Manager auto-enable")
|
||||||
prefs.manuallyDisabledTelecomManager = true
|
prefs.manuallyDisabledTelecomManager = true
|
||||||
}
|
}
|
||||||
prefs.useTelecomManager = newValue
|
prefs.useTelecomManager = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,7 +169,7 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
|
||||||
override fun onTextValueChanged(newValue: String) {
|
override fun onTextValueChanged(newValue: String) {
|
||||||
try {
|
try {
|
||||||
prefs.autoAnswerDelay = newValue.toInt()
|
prefs.autoAnswerDelay = newValue.toInt()
|
||||||
} catch (nfe: NumberFormatException) {
|
} catch (_: NumberFormatException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,7 +179,7 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
|
||||||
override fun onTextValueChanged(newValue: String) {
|
override fun onTextValueChanged(newValue: String) {
|
||||||
try {
|
try {
|
||||||
core.incTimeout = newValue.toInt()
|
core.incTimeout = newValue.toInt()
|
||||||
} catch (nfe: NumberFormatException) {
|
} catch (_: NumberFormatException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ class ChatSettingsViewModel : GenericSettingsViewModel() {
|
||||||
val maxSize = newValue.toInt()
|
val maxSize = newValue.toInt()
|
||||||
core.maxSizeForAutoDownloadIncomingFiles = maxSize
|
core.maxSizeForAutoDownloadIncomingFiles = maxSize
|
||||||
updateAutoDownloadIndexFromMaxSize(maxSize)
|
updateAutoDownloadIndexFromMaxSize(maxSize)
|
||||||
} catch (nfe: NumberFormatException) {
|
} catch (_: NumberFormatException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,8 @@ package org.linphone.activities.main.settings.viewmodels
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
import org.linphone.activities.main.settings.SettingListenerStub
|
import org.linphone.activities.main.settings.SettingListenerStub
|
||||||
|
import org.linphone.core.ConsolidatedPresence
|
||||||
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.utils.Event
|
import org.linphone.utils.Event
|
||||||
import org.linphone.utils.PermissionHelper
|
import org.linphone.utils.PermissionHelper
|
||||||
|
|
||||||
|
@ -40,6 +42,30 @@ class ContactsSettingsViewModel : GenericSettingsViewModel() {
|
||||||
val friendListSubscribe = MutableLiveData<Boolean>()
|
val friendListSubscribe = MutableLiveData<Boolean>()
|
||||||
val rlsAddressAvailable = MutableLiveData<Boolean>()
|
val rlsAddressAvailable = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val publishPresenceListener = object : SettingListenerStub() {
|
||||||
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
|
prefs.publishPresence = newValue
|
||||||
|
|
||||||
|
if (newValue) {
|
||||||
|
// Publish online presence when enabling setting
|
||||||
|
Log.i(
|
||||||
|
"[Contacts Settings] Presence has been enabled, PUBLISHING presence as Online"
|
||||||
|
)
|
||||||
|
core.consolidatedPresence = ConsolidatedPresence.Online
|
||||||
|
} else {
|
||||||
|
// Unpublish presence when disabling setting
|
||||||
|
Log.i("[Contacts Settings] Presence has been disabled, un-PUBLISHING presence info")
|
||||||
|
core.consolidatedPresence = ConsolidatedPresence.Offline
|
||||||
|
}
|
||||||
|
|
||||||
|
publishPresenceToggledEvent.value = Event(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val publishPresence = MutableLiveData<Boolean>()
|
||||||
|
val publishPresenceToggledEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||||
|
MutableLiveData<Event<Boolean>>()
|
||||||
|
}
|
||||||
|
|
||||||
val showNewContactAccountDialogListener = object : SettingListenerStub() {
|
val showNewContactAccountDialogListener = object : SettingListenerStub() {
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
prefs.showNewContactAccountDialog = newValue
|
prefs.showNewContactAccountDialog = newValue
|
||||||
|
@ -51,12 +77,12 @@ class ContactsSettingsViewModel : GenericSettingsViewModel() {
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
if (PermissionHelper.get().hasWriteContactsPermission()) {
|
if (PermissionHelper.get().hasWriteContactsPermission()) {
|
||||||
prefs.storePresenceInNativeContact = newValue
|
prefs.storePresenceInNativeContact = true
|
||||||
} else {
|
} else {
|
||||||
askWriteContactsPermissionForPresenceStorageEvent.value = Event(true)
|
askWriteContactsPermissionForPresenceStorageEvent.value = Event(true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
prefs.storePresenceInNativeContact = newValue
|
prefs.storePresenceInNativeContact = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,6 +123,8 @@ class ContactsSettingsViewModel : GenericSettingsViewModel() {
|
||||||
|
|
||||||
friendListSubscribe.value = core.isFriendListSubscriptionEnabled
|
friendListSubscribe.value = core.isFriendListSubscriptionEnabled
|
||||||
rlsAddressAvailable.value = !core.config.getString("sip", "rls_uri", "").isNullOrEmpty()
|
rlsAddressAvailable.value = !core.config.getString("sip", "rls_uri", "").isNullOrEmpty()
|
||||||
|
publishPresence.value = prefs.publishPresence
|
||||||
|
|
||||||
showNewContactAccountDialog.value = prefs.showNewContactAccountDialog
|
showNewContactAccountDialog.value = prefs.showNewContactAccountDialog
|
||||||
nativePresence.value = prefs.storePresenceInNativeContact
|
nativePresence.value = prefs.storePresenceInNativeContact
|
||||||
showOrganization.value = prefs.displayOrganization
|
showOrganization.value = prefs.displayOrganization
|
||||||
|
|
|
@ -52,7 +52,7 @@ class NetworkSettingsViewModel : GenericSettingsViewModel() {
|
||||||
try {
|
try {
|
||||||
val port = newValue.toInt()
|
val port = newValue.toInt()
|
||||||
setTransportPort(port)
|
setTransportPort(port)
|
||||||
} catch (nfe: NumberFormatException) {
|
} catch (_: NumberFormatException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ class TunnelSettingsViewModel : GenericSettingsViewModel() {
|
||||||
val config = getTunnelConfig()
|
val config = getTunnelConfig()
|
||||||
config.port = newValue.toInt()
|
config.port = newValue.toInt()
|
||||||
updateTunnelConfig(config)
|
updateTunnelConfig(config)
|
||||||
} catch (nfe: NumberFormatException) {
|
} catch (_: NumberFormatException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ class TunnelSettingsViewModel : GenericSettingsViewModel() {
|
||||||
val config = getTunnelConfig()
|
val config = getTunnelConfig()
|
||||||
config.port2 = newValue.toInt()
|
config.port2 = newValue.toInt()
|
||||||
updateTunnelConfig(config)
|
updateTunnelConfig(config)
|
||||||
} catch (nfe: NumberFormatException) {
|
} catch (_: NumberFormatException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,7 +87,17 @@ class VideoSettingsViewModel : GenericSettingsViewModel() {
|
||||||
val videoPresetListener = object : SettingListenerStub() {
|
val videoPresetListener = object : SettingListenerStub() {
|
||||||
override fun onListValueChanged(position: Int) {
|
override fun onListValueChanged(position: Int) {
|
||||||
videoPresetIndex.value = position // Needed to display/hide two below settings
|
videoPresetIndex.value = position // Needed to display/hide two below settings
|
||||||
core.videoPreset = videoPresetLabels.value.orEmpty()[position]
|
val currentPreset = core.videoPreset
|
||||||
|
val newPreset = videoPresetLabels.value.orEmpty()[position]
|
||||||
|
if (newPreset != currentPreset) {
|
||||||
|
if (currentPreset == "custom") {
|
||||||
|
// Not "custom" anymore, reset FPS & bandwidth
|
||||||
|
core.preferredFramerate = 0f
|
||||||
|
core.downloadBandwidth = 0
|
||||||
|
core.uploadBandwidth = 0
|
||||||
|
}
|
||||||
|
core.videoPreset = newPreset
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val videoPresetIndex = MutableLiveData<Int>()
|
val videoPresetIndex = MutableLiveData<Int>()
|
||||||
|
@ -106,7 +116,7 @@ class VideoSettingsViewModel : GenericSettingsViewModel() {
|
||||||
try {
|
try {
|
||||||
core.downloadBandwidth = newValue.toInt()
|
core.downloadBandwidth = newValue.toInt()
|
||||||
core.uploadBandwidth = newValue.toInt()
|
core.uploadBandwidth = newValue.toInt()
|
||||||
} catch (nfe: NumberFormatException) {
|
} catch (_: NumberFormatException) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ class ListTopBarViewModel : ViewModel() {
|
||||||
val list = arrayListOf<Int>()
|
val list = arrayListOf<Int>()
|
||||||
|
|
||||||
selectedItems.value = list
|
selectedItems.value = list
|
||||||
isSelectionNotEmpty.value = list.isNotEmpty()
|
isSelectionNotEmpty.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onToggleSelect(position: Int) {
|
fun onToggleSelect(position: Int) {
|
||||||
|
|
|
@ -62,8 +62,6 @@ class CallsListFragment : GenericVideoPreviewFragment<VoipCallsListFragmentBindi
|
||||||
|
|
||||||
binding.controlsViewModel = controlsViewModel
|
binding.controlsViewModel = controlsViewModel
|
||||||
|
|
||||||
setupLocalViewPreview(binding.localPreviewVideoSurface, binding.switchCamera)
|
|
||||||
|
|
||||||
binding.setCancelClickListener {
|
binding.setCancelClickListener {
|
||||||
goBack()
|
goBack()
|
||||||
}
|
}
|
||||||
|
@ -96,6 +94,18 @@ class CallsListFragment : GenericVideoPreviewFragment<VoipCallsListFragmentBindi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
|
||||||
|
cleanUpLocalVideoPreview(binding.localPreviewVideoSurface)
|
||||||
|
}
|
||||||
|
|
||||||
private fun showCallMenu(anchor: View, callData: CallData) {
|
private fun showCallMenu(anchor: View, callData: CallData) {
|
||||||
val popupView: VoipCallContextMenuBindingImpl = DataBindingUtil.inflate(
|
val popupView: VoipCallContextMenuBindingImpl = DataBindingUtil.inflate(
|
||||||
LayoutInflater.from(requireContext()),
|
LayoutInflater.from(requireContext()),
|
||||||
|
|
|
@ -23,7 +23,6 @@ import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.navigation.navGraphViewModels
|
import androidx.navigation.navGraphViewModels
|
||||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
|
||||||
import org.linphone.R
|
import org.linphone.R
|
||||||
import org.linphone.activities.voip.ConferenceDisplayMode
|
import org.linphone.activities.voip.ConferenceDisplayMode
|
||||||
import org.linphone.activities.voip.viewmodels.ConferenceViewModel
|
import org.linphone.activities.voip.viewmodels.ConferenceViewModel
|
||||||
|
@ -45,8 +44,6 @@ class ConferenceLayoutFragment : GenericVideoPreviewFragment<VoipConferenceLayou
|
||||||
|
|
||||||
binding.controlsViewModel = controlsViewModel
|
binding.controlsViewModel = controlsViewModel
|
||||||
|
|
||||||
setupLocalViewPreview(binding.localPreviewVideoSurface, binding.switchCamera)
|
|
||||||
|
|
||||||
binding.setCancelClickListener {
|
binding.setCancelClickListener {
|
||||||
goBack()
|
goBack()
|
||||||
}
|
}
|
||||||
|
@ -84,7 +81,13 @@ class ConferenceLayoutFragment : GenericVideoPreviewFragment<VoipConferenceLayou
|
||||||
showTooManyParticipantsForMosaicLayoutDialog()
|
showTooManyParticipantsForMosaicLayoutDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface
|
setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
|
||||||
|
cleanUpLocalVideoPreview(binding.localPreviewVideoSurface)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showTooManyParticipantsForMosaicLayoutDialog() {
|
private fun showTooManyParticipantsForMosaicLayoutDialog() {
|
||||||
|
|
|
@ -23,7 +23,6 @@ import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.navigation.navGraphViewModels
|
import androidx.navigation.navGraphViewModels
|
||||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
|
||||||
import org.linphone.R
|
import org.linphone.R
|
||||||
import org.linphone.activities.navigateToAddParticipants
|
import org.linphone.activities.navigateToAddParticipants
|
||||||
import org.linphone.activities.voip.viewmodels.ConferenceViewModel
|
import org.linphone.activities.voip.viewmodels.ConferenceViewModel
|
||||||
|
@ -49,8 +48,6 @@ class ConferenceParticipantsFragment : GenericVideoPreviewFragment<VoipConferenc
|
||||||
|
|
||||||
binding.controlsViewModel = controlsViewModel
|
binding.controlsViewModel = controlsViewModel
|
||||||
|
|
||||||
setupLocalViewPreview(binding.localPreviewVideoSurface, binding.switchCamera)
|
|
||||||
|
|
||||||
conferenceViewModel.conferenceExists.observe(
|
conferenceViewModel.conferenceExists.observe(
|
||||||
viewLifecycleOwner
|
viewLifecycleOwner
|
||||||
) { exists ->
|
) { exists ->
|
||||||
|
@ -90,12 +87,13 @@ class ConferenceParticipantsFragment : GenericVideoPreviewFragment<VoipConferenc
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
skipEvents = false
|
skipEvents = false
|
||||||
coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface
|
setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
skipEvents = true
|
skipEvents = true
|
||||||
|
cleanUpLocalVideoPreview(binding.localPreviewVideoSurface)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,6 @@ abstract class GenericVideoPreviewFragment<T : ViewDataBinding> : GenericFragmen
|
||||||
private var switchY: Float = 0f
|
private var switchY: Float = 0f
|
||||||
|
|
||||||
private var switchCameraImageView: ImageView? = null
|
private var switchCameraImageView: ImageView? = null
|
||||||
private lateinit var videoPreviewTextureView: TextureView
|
|
||||||
|
|
||||||
private val previewTouchListener = View.OnTouchListener { view, event ->
|
private val previewTouchListener = View.OnTouchListener { view, event ->
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
|
@ -67,19 +66,13 @@ abstract class GenericVideoPreviewFragment<T : ViewDataBinding> : GenericFragmen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun setupLocalViewPreview(localVideoPreview: TextureView, switchCamera: ImageView?) {
|
protected fun setupLocalVideoPreview(localVideoPreview: TextureView, switchCamera: ImageView?) {
|
||||||
if (coreContext.core.currentCall?.currentParams?.isVideoEnabled == true) {
|
|
||||||
videoPreviewTextureView = localVideoPreview
|
|
||||||
switchCameraImageView = switchCamera
|
switchCameraImageView = switchCamera
|
||||||
videoPreviewTextureView.setOnTouchListener(previewTouchListener)
|
localVideoPreview.setOnTouchListener(previewTouchListener)
|
||||||
}
|
coreContext.core.nativePreviewWindowId = localVideoPreview
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
protected fun cleanUpLocalVideoPreview(localVideoPreview: TextureView) {
|
||||||
super.onResume()
|
localVideoPreview.setOnTouchListener(null)
|
||||||
|
|
||||||
if (::videoPreviewTextureView.isInitialized) {
|
|
||||||
coreContext.core.nativePreviewWindowId = videoPreviewTextureView
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ import android.os.SystemClock
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Chronometer
|
import android.widget.Chronometer
|
||||||
import androidx.navigation.navGraphViewModels
|
import androidx.navigation.navGraphViewModels
|
||||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
|
||||||
import org.linphone.R
|
import org.linphone.R
|
||||||
import org.linphone.activities.navigateToActiveCall
|
import org.linphone.activities.navigateToActiveCall
|
||||||
import org.linphone.activities.voip.viewmodels.CallsViewModel
|
import org.linphone.activities.voip.viewmodels.CallsViewModel
|
||||||
|
@ -46,8 +45,6 @@ class OutgoingCallFragment : GenericVideoPreviewFragment<VoipCallOutgoingFragmen
|
||||||
|
|
||||||
binding.callsViewModel = callsViewModel
|
binding.callsViewModel = callsViewModel
|
||||||
|
|
||||||
setupLocalViewPreview(binding.localPreviewVideoSurface, binding.switchCamera)
|
|
||||||
|
|
||||||
callsViewModel.callConnectedEvent.observe(
|
callsViewModel.callConnectedEvent.observe(
|
||||||
viewLifecycleOwner
|
viewLifecycleOwner
|
||||||
) {
|
) {
|
||||||
|
@ -79,7 +76,7 @@ class OutgoingCallFragment : GenericVideoPreviewFragment<VoipCallOutgoingFragmen
|
||||||
viewLifecycleOwner
|
viewLifecycleOwner
|
||||||
) {
|
) {
|
||||||
if (it) {
|
if (it) {
|
||||||
coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface
|
setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -87,11 +84,17 @@ class OutgoingCallFragment : GenericVideoPreviewFragment<VoipCallOutgoingFragmen
|
||||||
// We don't want the proximity sensor to turn screen OFF in this fragment
|
// We don't want the proximity sensor to turn screen OFF in this fragment
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
controlsViewModel.forceDisableProximitySensor.value = true
|
controlsViewModel.forceDisableProximitySensor.value = true
|
||||||
|
if (controlsViewModel.isOutgoingEarlyMedia.value == true) {
|
||||||
|
setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
controlsViewModel.forceDisableProximitySensor.value = false
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
|
controlsViewModel.forceDisableProximitySensor.value = false
|
||||||
|
cleanUpLocalVideoPreview(binding.localPreviewVideoSurface)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,8 +67,6 @@ class SingleCallFragment : GenericVideoPreviewFragment<VoipSingleCallFragmentBin
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
|
||||||
setupLocalViewPreview(binding.localPreviewVideoSurface, binding.switchCamera)
|
|
||||||
|
|
||||||
binding.controlsViewModel = controlsViewModel
|
binding.controlsViewModel = controlsViewModel
|
||||||
|
|
||||||
binding.callsViewModel = callsViewModel
|
binding.callsViewModel = callsViewModel
|
||||||
|
@ -89,7 +87,7 @@ class SingleCallFragment : GenericVideoPreviewFragment<VoipSingleCallFragmentBin
|
||||||
)
|
)
|
||||||
navigateToIncomingCall()
|
navigateToIncomingCall()
|
||||||
}
|
}
|
||||||
Call.State.OutgoingInit, Call.State.OutgoingProgress, Call.State.OutgoingRinging, Call.State.OutgoingEarlyMedia -> {
|
Call.State.OutgoingRinging, Call.State.OutgoingEarlyMedia -> {
|
||||||
Log.i(
|
Log.i(
|
||||||
"[Single Call] New current call is in [$callState] state, switching to OutgoingCall fragment"
|
"[Single Call] New current call is in [$callState] state, switching to OutgoingCall fragment"
|
||||||
)
|
)
|
||||||
|
@ -193,15 +191,20 @@ class SingleCallFragment : GenericVideoPreviewFragment<VoipSingleCallFragmentBin
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface
|
coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface
|
||||||
coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface
|
setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
controlsViewModel.hideExtraButtons(true)
|
controlsViewModel.hideExtraButtons(true)
|
||||||
|
cleanUpLocalVideoPreview(binding.localPreviewVideoSurface)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showCallVideoUpdateDialog(call: Call) {
|
private fun showCallVideoUpdateDialog(call: Call) {
|
||||||
|
|
|
@ -84,8 +84,11 @@ class StatusFragment : GenericFragment<VoipStatusFragmentBinding>() {
|
||||||
|
|
||||||
private fun showZrtpDialog(call: Call) {
|
private fun showZrtpDialog(call: Call) {
|
||||||
if (zrtpDialog != null && zrtpDialog?.isShowing == true) {
|
if (zrtpDialog != null && zrtpDialog?.isShowing == true) {
|
||||||
Log.e("[Status Fragment] ZRTP dialog already visible")
|
Log.w(
|
||||||
return
|
"[Status Fragment] ZRTP dialog already visible, closing it and creating a new one"
|
||||||
|
)
|
||||||
|
zrtpDialog?.dismiss()
|
||||||
|
zrtpDialog = null
|
||||||
}
|
}
|
||||||
|
|
||||||
val token = call.authenticationToken
|
val token = call.authenticationToken
|
||||||
|
@ -125,8 +128,19 @@ class StatusFragment : GenericFragment<VoipStatusFragmentBinding>() {
|
||||||
|
|
||||||
viewModel.showCancelButton(
|
viewModel.showCancelButton(
|
||||||
{
|
{
|
||||||
|
if (call.state != Call.State.End && call.state != Call.State.Released) {
|
||||||
|
if (call.authenticationTokenVerified) {
|
||||||
|
Log.w(
|
||||||
|
"[Status Fragment] Removing trust from previously verified ZRTP SAS auth token"
|
||||||
|
)
|
||||||
|
this@StatusFragment.viewModel.previouslyDeclineToken = true
|
||||||
call.authenticationTokenVerified = false
|
call.authenticationTokenVerified = false
|
||||||
this@StatusFragment.viewModel.updateEncryptionInfo(call)
|
}
|
||||||
|
} else {
|
||||||
|
Log.e(
|
||||||
|
"[Status Fragment] Can't decline the ZRTP SAS token, call is in state [${call.state}]"
|
||||||
|
)
|
||||||
|
}
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
zrtpDialog = null
|
zrtpDialog = null
|
||||||
},
|
},
|
||||||
|
@ -135,8 +149,13 @@ class StatusFragment : GenericFragment<VoipStatusFragmentBinding>() {
|
||||||
|
|
||||||
viewModel.showOkButton(
|
viewModel.showOkButton(
|
||||||
{
|
{
|
||||||
|
if (call.state != Call.State.End && call.state != Call.State.Released) {
|
||||||
call.authenticationTokenVerified = true
|
call.authenticationTokenVerified = true
|
||||||
this@StatusFragment.viewModel.updateEncryptionInfo(call)
|
} else {
|
||||||
|
Log.e(
|
||||||
|
"[Status Fragment] Can't verify the ZRTP SAS token, call is in state [${call.state}]"
|
||||||
|
)
|
||||||
|
}
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
zrtpDialog = null
|
zrtpDialog = null
|
||||||
},
|
},
|
||||||
|
|
|
@ -81,6 +81,8 @@ class ControlsViewModel : ViewModel() {
|
||||||
|
|
||||||
val attendedTransfer = MutableLiveData<Boolean>()
|
val attendedTransfer = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val chatDisabled = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
val goToConferenceParticipantsListEvent: MutableLiveData<Event<Boolean>> by lazy {
|
val goToConferenceParticipantsListEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||||
MutableLiveData<Event<Boolean>>()
|
MutableLiveData<Event<Boolean>>()
|
||||||
}
|
}
|
||||||
|
@ -206,6 +208,7 @@ class ControlsViewModel : ViewModel() {
|
||||||
init {
|
init {
|
||||||
coreContext.core.addListener(listener)
|
coreContext.core.addListener(listener)
|
||||||
|
|
||||||
|
chatDisabled.value = corePreferences.disableChat
|
||||||
fullScreenMode.value = false
|
fullScreenMode.value = false
|
||||||
extraButtonsMenuTranslateY.value = AppUtils.getDimension(
|
extraButtonsMenuTranslateY.value = AppUtils.getDimension(
|
||||||
R.dimen.voip_call_extra_buttons_translate_y
|
R.dimen.voip_call_extra_buttons_translate_y
|
||||||
|
@ -536,7 +539,7 @@ class ControlsViewModel : ViewModel() {
|
||||||
|
|
||||||
isVideoEnabled.value = enabled
|
isVideoEnabled.value = enabled
|
||||||
showTakeSnapshotButton.value = enabled && corePreferences.showScreenshotButton
|
showTakeSnapshotButton.value = enabled && corePreferences.showScreenshotButton
|
||||||
var isVideoBeingSent = if (coreContext.core.currentCall?.conference != null) {
|
val isVideoBeingSent = if (coreContext.core.currentCall?.conference != null) {
|
||||||
val videoDirection = coreContext.core.currentCall?.currentParams?.videoDirection
|
val videoDirection = coreContext.core.currentCall?.currentParams?.videoDirection
|
||||||
videoDirection == MediaDirection.SendRecv || videoDirection == MediaDirection.SendOnly
|
videoDirection == MediaDirection.SendRecv || videoDirection == MediaDirection.SendOnly
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -42,6 +42,8 @@ class StatusViewModel : StatusViewModel() {
|
||||||
MutableLiveData<Event<Boolean>>()
|
MutableLiveData<Event<Boolean>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var previouslyDeclineToken = false
|
||||||
|
|
||||||
private val listener = object : CoreListenerStub() {
|
private val listener = object : CoreListenerStub() {
|
||||||
override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) {
|
override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) {
|
||||||
updateCallQualityIcon()
|
updateCallQualityIcon()
|
||||||
|
@ -54,8 +56,11 @@ class StatusViewModel : StatusViewModel() {
|
||||||
authenticationToken: String?
|
authenticationToken: String?
|
||||||
) {
|
) {
|
||||||
updateEncryptionInfo(call)
|
updateEncryptionInfo(call)
|
||||||
if (call.currentParams.mediaEncryption == MediaEncryption.ZRTP && !call.authenticationTokenVerified && call.authenticationToken != null) {
|
// Check if we just declined a previously validated token
|
||||||
showZrtpDialogEvent.value = Event(call)
|
// In that case, don't show the ZRTP dialog again
|
||||||
|
if (!previouslyDeclineToken) {
|
||||||
|
previouslyDeclineToken = false
|
||||||
|
showZrtpDialog(call)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,10 +84,7 @@ class StatusViewModel : StatusViewModel() {
|
||||||
val currentCall = coreContext.core.currentCall
|
val currentCall = coreContext.core.currentCall
|
||||||
if (currentCall != null) {
|
if (currentCall != null) {
|
||||||
updateEncryptionInfo(currentCall)
|
updateEncryptionInfo(currentCall)
|
||||||
|
showZrtpDialog(currentCall)
|
||||||
if (currentCall.currentParams.mediaEncryption == MediaEncryption.ZRTP && !currentCall.authenticationTokenVerified && currentCall.authenticationToken != null) {
|
|
||||||
showZrtpDialogEvent.value = Event(currentCall)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,8 +96,8 @@ class StatusViewModel : StatusViewModel() {
|
||||||
|
|
||||||
fun showZrtpDialog() {
|
fun showZrtpDialog() {
|
||||||
val currentCall = coreContext.core.currentCall
|
val currentCall = coreContext.core.currentCall
|
||||||
if (currentCall?.authenticationToken != null && currentCall.currentParams.mediaEncryption == MediaEncryption.ZRTP) {
|
if (currentCall != null) {
|
||||||
showZrtpDialogEvent.value = Event(currentCall)
|
showZrtpDialog(currentCall, force = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,6 +114,9 @@ class StatusViewModel : StatusViewModel() {
|
||||||
encryptionContentDescription.value = R.string.content_description_call_secured
|
encryptionContentDescription.value = R.string.content_description_call_secured
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (call.state == Call.State.End || call.state == Call.State.Released) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
when (call.currentParams.mediaEncryption ?: MediaEncryption.None) {
|
when (call.currentParams.mediaEncryption ?: MediaEncryption.None) {
|
||||||
MediaEncryption.SRTP, MediaEncryption.DTLS -> {
|
MediaEncryption.SRTP, MediaEncryption.DTLS -> {
|
||||||
|
@ -139,6 +144,16 @@ class StatusViewModel : StatusViewModel() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showZrtpDialog(call: Call, force: Boolean = false) {
|
||||||
|
if (
|
||||||
|
call.currentParams.mediaEncryption == MediaEncryption.ZRTP &&
|
||||||
|
call.authenticationToken != null &&
|
||||||
|
(!call.authenticationTokenVerified || force)
|
||||||
|
) {
|
||||||
|
showZrtpDialogEvent.value = Event(call)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateCallQualityIcon() {
|
private fun updateCallQualityIcon() {
|
||||||
val call = coreContext.core.currentCall ?: coreContext.core.calls.firstOrNull()
|
val call = coreContext.core.currentCall ?: coreContext.core.calls.firstOrNull()
|
||||||
val quality = call?.currentQuality ?: 0f
|
val quality = call?.currentQuality ?: 0f
|
||||||
|
|
|
@ -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)
|
context.startForegroundService(intent)
|
||||||
} catch (fssnae: ForegroundServiceStartNotAllowedException) {
|
} catch (fssnae: ForegroundServiceStartNotAllowedException) {
|
||||||
Log.e("[Api31 Compatibility] Can't start service as foreground! $fssnae")
|
Log.e("[Api31 Compatibility] Can't start service as foreground! $fssnae")
|
||||||
|
} catch (se: SecurityException) {
|
||||||
|
Log.e("[Api31 Compatibility] Can't start service as foreground! $se")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("[Api31 Compatibility] Can't start service as foreground! $e")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,6 +274,10 @@ class Api31Compatibility {
|
||||||
service.startForeground(notifId, notif)
|
service.startForeground(notifId, notif)
|
||||||
} catch (fssnae: ForegroundServiceStartNotAllowedException) {
|
} catch (fssnae: ForegroundServiceStartNotAllowedException) {
|
||||||
Log.e("[Api31 Compatibility] Can't start service as foreground! $fssnae")
|
Log.e("[Api31 Compatibility] Can't start service as foreground! $fssnae")
|
||||||
|
} catch (se: SecurityException) {
|
||||||
|
Log.e("[Api31 Compatibility] Can't start service as foreground! $se")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("[Api31 Compatibility] Can't start service as foreground! $e")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.Notification
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
@ -290,7 +291,7 @@ class Compatibility {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startForegroundService(service: Service, notifId: Int, notif: Notification?) {
|
private fun startForegroundService(service: Service, notifId: Int, notif: Notification?) {
|
||||||
if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
|
if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
|
||||||
Api31Compatibility.startForegroundService(service, notifId, notif)
|
Api31Compatibility.startForegroundService(service, notifId, notif)
|
||||||
} else {
|
} else {
|
||||||
|
@ -298,6 +299,22 @@ class Compatibility {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startCallForegroundService(service: Service, notifId: Int, notif: Notification) {
|
||||||
|
if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) {
|
||||||
|
Api34Compatibility.startCallForegroundService(service, notifId, notif)
|
||||||
|
} else {
|
||||||
|
startForegroundService(service, notifId, notif)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startDataSyncForegroundService(service: Service, notifId: Int, notif: Notification) {
|
||||||
|
if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) {
|
||||||
|
Api34Compatibility.startDataSyncForegroundService(service, notifId, notif)
|
||||||
|
} else {
|
||||||
|
startForegroundService(service, notifId, notif)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Call */
|
/* Call */
|
||||||
|
|
||||||
fun canDrawOverlay(context: Context): Boolean {
|
fun canDrawOverlay(context: Context): Boolean {
|
||||||
|
@ -455,5 +472,26 @@ class Compatibility {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearClipboard(clipboard: ClipboardManager) {
|
||||||
|
if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) {
|
||||||
|
Api28Compatibility.clearClipboard(clipboard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasFullScreenIntentPermission(context: Context): Boolean {
|
||||||
|
if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) {
|
||||||
|
return Api34Compatibility.hasFullScreenIntentPermission(context)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestFullScreenIntentPermission(context: Context): Boolean {
|
||||||
|
if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) {
|
||||||
|
Api34Compatibility.requestFullScreenIntentPermission(context)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,7 @@ abstract class GenericContactViewModel(private val sipAddress: Address) : Messag
|
||||||
contactLookup()
|
contactLookup()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun contactLookup() {
|
private fun contactLookup() {
|
||||||
displayName.value = LinphoneUtils.getDisplayName(sipAddress)
|
displayName.value = LinphoneUtils.getDisplayName(sipAddress)
|
||||||
val friend = coreContext.contactsManager.findContactByAddress(sipAddress)
|
val friend = coreContext.contactsManager.findContactByAddress(sipAddress)
|
||||||
if (friend != null) {
|
if (friend != null) {
|
||||||
|
|
|
@ -35,7 +35,6 @@ import androidx.core.app.Person
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||||
import org.linphone.R
|
import org.linphone.R
|
||||||
|
@ -388,7 +387,7 @@ fun Friend.getContactForPhoneNumberOrAddress(value: String): String? {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Friend.hasPresence(): Boolean {
|
fun Friend.hasLongTermPresence(): Boolean {
|
||||||
for (address in addresses) {
|
for (address in addresses) {
|
||||||
val presenceModel = getPresenceModelForUriOrTel(address.asStringUriOnly())
|
val presenceModel = getPresenceModelForUriOrTel(address.asStringUriOnly())
|
||||||
if (presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open) return true
|
if (presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open) return true
|
||||||
|
@ -421,10 +420,12 @@ fun Friend.getPictureUri(thumbnailPreferred: Boolean = false): Uri? {
|
||||||
// Check that the URI points to a real file
|
// Check that the URI points to a real file
|
||||||
val contentResolver = coreContext.context.contentResolver
|
val contentResolver = coreContext.context.contentResolver
|
||||||
try {
|
try {
|
||||||
if (contentResolver.openAssetFileDescriptor(pictureUri, "r") != null) {
|
val fd = contentResolver.openAssetFileDescriptor(pictureUri, "r")
|
||||||
|
if (fd != null) {
|
||||||
|
fd.close()
|
||||||
return pictureUri
|
return pictureUri
|
||||||
}
|
}
|
||||||
} catch (ioe: IOException) { }
|
} catch (_: IOException) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to thumbnail if high res picture isn't available
|
// Fallback to thumbnail if high res picture isn't available
|
||||||
|
@ -432,11 +433,11 @@ fun Friend.getPictureUri(thumbnailPreferred: Boolean = false): Uri? {
|
||||||
lookupUri,
|
lookupUri,
|
||||||
ContactsContract.Contacts.Photo.CONTENT_DIRECTORY
|
ContactsContract.Contacts.Photo.CONTENT_DIRECTORY
|
||||||
)
|
)
|
||||||
} catch (e: Exception) { }
|
} catch (_: Exception) { }
|
||||||
} else if (photo != null) {
|
} else if (photo != null) {
|
||||||
try {
|
try {
|
||||||
return Uri.parse(photo)
|
return Uri.parse(photo)
|
||||||
} catch (e: Exception) { }
|
} catch (_: Exception) { }
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -165,12 +165,6 @@ class CoreContext(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAuthenticationRequested(core: Core, authInfo: AuthInfo, method: AuthMethod) {
|
|
||||||
Log.w(
|
|
||||||
"[Context] Authentication requested for account [${authInfo.username}@${authInfo.domain}] with realm [${authInfo.realm}] using method [$method]"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPushNotificationReceived(core: Core, payload: String?) {
|
override fun onPushNotificationReceived(core: Core, payload: String?) {
|
||||||
Log.i("[Context] Push notification received: $payload")
|
Log.i("[Context] Push notification received: $payload")
|
||||||
}
|
}
|
||||||
|
@ -431,18 +425,22 @@ class CoreContext(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onForeground() {
|
fun onForeground() {
|
||||||
// If presence publish is disabled and we call core.setConsolidatedPresence, it will enabled it!
|
// We can't rely on defaultAccount?.params?.isPublishEnabled
|
||||||
if (core.defaultAccount?.params?.isPublishEnabled == true) {
|
// as it will be modified by the SDK when changing the presence status
|
||||||
Log.i("[Context] App is in foreground, setting consolidated presence to Online")
|
if (corePreferences.publishPresence) {
|
||||||
|
Log.i("[Context] App is in foreground, PUBLISHING presence as Online")
|
||||||
core.consolidatedPresence = ConsolidatedPresence.Online
|
core.consolidatedPresence = ConsolidatedPresence.Online
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onBackground() {
|
fun onBackground() {
|
||||||
// If presence publish is disabled and we call core.setConsolidatedPresence, it will enabled it!
|
// We can't rely on defaultAccount?.params?.isPublishEnabled
|
||||||
if (core.defaultAccount?.params?.isPublishEnabled == true) {
|
// as it will be modified by the SDK when changing the presence status
|
||||||
Log.i("[Context] App is in background, setting consolidated presence to Busy")
|
if (corePreferences.publishPresence) {
|
||||||
core.consolidatedPresence = ConsolidatedPresence.Busy
|
Log.i("[Context] App is in background, un-PUBLISHING presence info")
|
||||||
|
// We don't use ConsolidatedPresence.Busy but Offline to do an unsubscribe,
|
||||||
|
// Flexisip will handle the Busy status depending on other devices
|
||||||
|
core.consolidatedPresence = ConsolidatedPresence.Offline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1039,8 +1037,8 @@ class CoreContext(
|
||||||
|
|
||||||
val extension = FileUtils.getExtensionFromFileName(filePath)
|
val extension = FileUtils.getExtensionFromFileName(filePath)
|
||||||
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||||
when {
|
when (FileUtils.getMimeType(mime)) {
|
||||||
FileUtils.isMimeImage(mime) -> {
|
FileUtils.MimeType.Image -> {
|
||||||
if (Compatibility.addImageToMediaStore(context, content)) {
|
if (Compatibility.addImageToMediaStore(context, content)) {
|
||||||
Log.i(
|
Log.i(
|
||||||
"[Context] Successfully exported image [${content.name}] to Media Store"
|
"[Context] Successfully exported image [${content.name}] to Media Store"
|
||||||
|
@ -1051,7 +1049,7 @@ class CoreContext(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileUtils.isMimeVideo(mime) -> {
|
FileUtils.MimeType.Video -> {
|
||||||
if (Compatibility.addVideoToMediaStore(context, content)) {
|
if (Compatibility.addVideoToMediaStore(context, content)) {
|
||||||
Log.i(
|
Log.i(
|
||||||
"[Context] Successfully exported video [${content.name}] to Media Store"
|
"[Context] Successfully exported video [${content.name}] to Media Store"
|
||||||
|
@ -1062,7 +1060,7 @@ class CoreContext(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileUtils.isMimeAudio(mime) -> {
|
FileUtils.MimeType.Audio -> {
|
||||||
if (Compatibility.addAudioToMediaStore(context, content)) {
|
if (Compatibility.addAudioToMediaStore(context, content)) {
|
||||||
Log.i(
|
Log.i(
|
||||||
"[Context] Successfully exported audio [${content.name}] to Media Store"
|
"[Context] Successfully exported audio [${content.name}] to Media Store"
|
||||||
|
|
|
@ -53,6 +53,7 @@ class CorePreferences constructor(private val context: Context) {
|
||||||
context,
|
context,
|
||||||
MasterKey.DEFAULT_MASTER_KEY_ALIAS
|
MasterKey.DEFAULT_MASTER_KEY_ALIAS
|
||||||
).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
|
).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
EncryptedSharedPreferences.create(
|
EncryptedSharedPreferences.create(
|
||||||
context,
|
context,
|
||||||
|
@ -280,6 +281,12 @@ class CorePreferences constructor(private val context: Context) {
|
||||||
config.setBool("app", "contact_shortcuts", value)
|
config.setBool("app", "contact_shortcuts", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var publishPresence: Boolean
|
||||||
|
get() = config.getBool("app", "publish_presence", true)
|
||||||
|
set(value) {
|
||||||
|
config.setBool("app", "publish_presence", value)
|
||||||
|
}
|
||||||
|
|
||||||
/* Call */
|
/* Call */
|
||||||
|
|
||||||
var sendEarlyMedia: Boolean
|
var sendEarlyMedia: Boolean
|
||||||
|
|
|
@ -34,27 +34,33 @@ class CoreService : CoreService() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
Log.i("[Service] Ensuring Core exists")
|
Log.i("[Service] Starting, ensuring Core exists")
|
||||||
|
|
||||||
if (corePreferences.keepServiceAlive) {
|
if (corePreferences.keepServiceAlive) {
|
||||||
Log.i("[Service] Starting as foreground to keep app alive in background")
|
Log.i("[Service] Starting as foreground to keep app alive in background")
|
||||||
if (!ensureCoreExists(
|
val contextCreated = ensureCoreExists(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
pushReceived = false,
|
pushReceived = false,
|
||||||
service = this,
|
service = this,
|
||||||
useAutoStartDescription = false
|
useAutoStartDescription = false
|
||||||
)
|
)
|
||||||
) {
|
if (!contextCreated) {
|
||||||
|
// Only start foreground notification if context already exists, otherwise context will do it itself
|
||||||
coreContext.notificationsManager.startForeground(this, false)
|
coreContext.notificationsManager.startForeground(this, false)
|
||||||
}
|
}
|
||||||
} else if (intent?.extras?.get("StartForeground") == true) {
|
} else if (intent?.extras?.get("StartForeground") == true) {
|
||||||
Log.i("[Service] Starting as foreground due to device boot or app update")
|
Log.i("[Service] Starting as foreground due to device boot or app update")
|
||||||
if (!ensureCoreExists(
|
val contextCreated = ensureCoreExists(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
pushReceived = false,
|
pushReceived = false,
|
||||||
service = this,
|
service = this,
|
||||||
useAutoStartDescription = true
|
useAutoStartDescription = true,
|
||||||
|
skipCoreStart = true
|
||||||
)
|
)
|
||||||
) {
|
if (contextCreated) {
|
||||||
|
coreContext.start()
|
||||||
|
} else {
|
||||||
|
// Only start foreground notification if context already exists, otherwise context will do it itself
|
||||||
coreContext.notificationsManager.startForeground(this, true)
|
coreContext.notificationsManager.startForeground(this, true)
|
||||||
}
|
}
|
||||||
coreContext.checkIfForegroundServiceNotificationCanBeRemovedAfterDelay(5000)
|
coreContext.checkIfForegroundServiceNotificationCanBeRemovedAfterDelay(5000)
|
||||||
|
|
|
@ -84,7 +84,7 @@ class NotificationsManager(private val context: Context) {
|
||||||
const val INTENT_REMOTE_ADDRESS = "REMOTE_ADDRESS"
|
const val INTENT_REMOTE_ADDRESS = "REMOTE_ADDRESS"
|
||||||
|
|
||||||
private const val SERVICE_NOTIF_ID = 1
|
private const val SERVICE_NOTIF_ID = 1
|
||||||
private const val MISSED_CALLS_NOTIF_ID = 2
|
private const val MISSED_CALLS_NOTIF_ID = 10
|
||||||
|
|
||||||
const val CHAT_TAG = "Chat"
|
const val CHAT_TAG = "Chat"
|
||||||
private const val MISSED_CALL_TAG = "Missed call"
|
private const val MISSED_CALL_TAG = "Missed call"
|
||||||
|
@ -179,7 +179,23 @@ class NotificationsManager(private val context: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val notifiable = createChatNotifiable(room, messages)
|
var allOutgoing = true
|
||||||
|
for (message in messages) {
|
||||||
|
if (!message.isOutgoing) {
|
||||||
|
allOutgoing = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val notifiable = getNotifiableForRoom(room)
|
||||||
|
val updated = updateChatNotifiableWithMessages(notifiable, room, messages)
|
||||||
|
if (!updated) {
|
||||||
|
Log.w(
|
||||||
|
"[Notifications Manager] No changes made to notifiable, do not display it again"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (notifiable.messages.isNotEmpty()) {
|
if (notifiable.messages.isNotEmpty()) {
|
||||||
displayChatNotifiable(room, notifiable)
|
displayChatNotifiable(room, notifiable)
|
||||||
} else {
|
} else {
|
||||||
|
@ -333,7 +349,7 @@ class NotificationsManager(private val context: Context) {
|
||||||
Log.w(
|
Log.w(
|
||||||
"[Notifications Manager] Found existing call? notification [${notification.id}], cancelling it"
|
"[Notifications Manager] Found existing call? notification [${notification.id}], cancelling it"
|
||||||
)
|
)
|
||||||
manager.cancel(notification.tag, notification.id)
|
manager.cancel(notification.id)
|
||||||
} else if (notification.tag == CHAT_TAG) {
|
} else if (notification.tag == CHAT_TAG) {
|
||||||
Log.i(
|
Log.i(
|
||||||
"[Notifications Manager] Found existing chat notification [${notification.id}]"
|
"[Notifications Manager] Found existing chat notification [${notification.id}]"
|
||||||
|
@ -381,7 +397,18 @@ class NotificationsManager(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.i("[Notifications Manager] Notifying [$id] with tag [$tag]")
|
Log.i("[Notifications Manager] Notifying [$id] with tag [$tag]")
|
||||||
|
try {
|
||||||
notificationManager.notify(tag, id, notification)
|
notificationManager.notify(tag, id, notification)
|
||||||
|
} catch (iae: IllegalArgumentException) {
|
||||||
|
if (service == null && tag == null) {
|
||||||
|
// We can't notify using CallStyle if there isn't a foreground service running
|
||||||
|
Log.w(
|
||||||
|
"[Notifications Manager] Foreground service hasn't started yet, can't display a CallStyle notification until then: $iae"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Log.e("[Notifications Manager] Exception occurred: $iae")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cancel(id: Int, tag: String? = null) {
|
fun cancel(id: Int, tag: String? = null) {
|
||||||
|
@ -460,24 +487,23 @@ class NotificationsManager(private val context: Context) {
|
||||||
fun startForeground(coreService: CoreService, useAutoStartDescription: Boolean = true) {
|
fun startForeground(coreService: CoreService, useAutoStartDescription: Boolean = true) {
|
||||||
service = coreService
|
service = coreService
|
||||||
|
|
||||||
if (serviceNotification == null) {
|
val notification = serviceNotification ?: createServiceNotification(useAutoStartDescription)
|
||||||
createServiceNotification(useAutoStartDescription)
|
if (notification == null) {
|
||||||
if (serviceNotification == null) {
|
|
||||||
Log.e(
|
Log.e(
|
||||||
"[Notifications Manager] Failed to create service notification, aborting foreground service!"
|
"[Notifications Manager] Failed to create service notification, aborting foreground service!"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
currentForegroundServiceNotificationId = SERVICE_NOTIF_ID
|
currentForegroundServiceNotificationId = SERVICE_NOTIF_ID
|
||||||
Log.i(
|
Log.i(
|
||||||
"[Notifications Manager] Starting service as foreground [$currentForegroundServiceNotificationId]"
|
"[Notifications Manager] Starting service as foreground [$currentForegroundServiceNotificationId]"
|
||||||
)
|
)
|
||||||
Compatibility.startForegroundService(
|
|
||||||
|
Compatibility.startDataSyncForegroundService(
|
||||||
coreService,
|
coreService,
|
||||||
currentForegroundServiceNotificationId,
|
currentForegroundServiceNotificationId,
|
||||||
serviceNotification
|
notification
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -491,7 +517,7 @@ class NotificationsManager(private val context: Context) {
|
||||||
|
|
||||||
val coreService = service
|
val coreService = service
|
||||||
if (coreService != null) {
|
if (coreService != null) {
|
||||||
Compatibility.startForegroundService(
|
Compatibility.startCallForegroundService(
|
||||||
coreService,
|
coreService,
|
||||||
currentForegroundServiceNotificationId,
|
currentForegroundServiceNotificationId,
|
||||||
callNotification
|
callNotification
|
||||||
|
@ -552,11 +578,11 @@ class NotificationsManager(private val context: Context) {
|
||||||
service = null
|
service = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createServiceNotification(useAutoStartDescription: Boolean = false) {
|
private fun createServiceNotification(useAutoStartDescription: Boolean = false): Notification? {
|
||||||
val serviceChannel = context.getString(R.string.notification_channel_service_id)
|
val serviceChannel = context.getString(R.string.notification_channel_service_id)
|
||||||
if (Compatibility.getChannelImportance(notificationManager, serviceChannel) == NotificationManagerCompat.IMPORTANCE_NONE) {
|
if (Compatibility.getChannelImportance(notificationManager, serviceChannel) == NotificationManagerCompat.IMPORTANCE_NONE) {
|
||||||
Log.w("[Notifications Manager] Service channel is disabled!")
|
Log.w("[Notifications Manager] Service channel is disabled!")
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val pendingIntent = NavDeepLinkBuilder(context)
|
val pendingIntent = NavDeepLinkBuilder(context)
|
||||||
|
@ -588,7 +614,9 @@ class NotificationsManager(private val context: Context) {
|
||||||
builder.setContentIntent(pendingIntent)
|
builder.setContentIntent(pendingIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceNotification = builder.build()
|
val notif = builder.build()
|
||||||
|
serviceNotification = notif
|
||||||
|
return notif
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Call related */
|
/* Call related */
|
||||||
|
@ -846,14 +874,18 @@ class NotificationsManager(private val context: Context) {
|
||||||
notify(notifiable.notificationId, notification, CHAT_TAG)
|
notify(notifiable.notificationId, notification, CHAT_TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createChatNotifiable(room: ChatRoom, messages: Array<out ChatMessage>): Notifiable {
|
private fun updateChatNotifiableWithMessages(
|
||||||
val notifiable = getNotifiableForRoom(room)
|
notifiable: Notifiable,
|
||||||
|
room: ChatRoom,
|
||||||
|
messages: Array<out ChatMessage>
|
||||||
|
): Boolean {
|
||||||
|
var updated = false
|
||||||
for (message in messages) {
|
for (message in messages) {
|
||||||
if (message.isRead || message.isOutgoing) continue
|
if (message.isRead || message.isOutgoing) continue
|
||||||
val friend = coreContext.contactsManager.findContactByAddress(message.fromAddress)
|
val friend = coreContext.contactsManager.findContactByAddress(message.fromAddress)
|
||||||
val notifiableMessage = getNotifiableMessage(message, friend)
|
val notifiableMessage = getNotifiableMessage(message, friend)
|
||||||
notifiable.messages.add(notifiableMessage)
|
notifiable.messages.add(notifiableMessage)
|
||||||
|
updated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) {
|
if (room.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) {
|
||||||
|
@ -862,7 +894,7 @@ class NotificationsManager(private val context: Context) {
|
||||||
notifiable.isGroup = true
|
notifiable.isGroup = true
|
||||||
notifiable.groupTitle = room.subject
|
notifiable.groupTitle = room.subject
|
||||||
}
|
}
|
||||||
return notifiable
|
return updated
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createChatReactionNotifiable(
|
private fun createChatReactionNotifiable(
|
||||||
|
|
|
@ -44,6 +44,10 @@ import org.linphone.core.tools.Log
|
||||||
* Various utility methods for application
|
* Various utility methods for application
|
||||||
*/
|
*/
|
||||||
class AppUtils {
|
class AppUtils {
|
||||||
|
interface KeyboardVisibilityListener {
|
||||||
|
fun onKeyboardVisibilityChanged(visible: Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val emojiCompat: EmojiCompat?
|
private val emojiCompat: EmojiCompat?
|
||||||
get() = initEmojiCompat()
|
get() = initEmojiCompat()
|
||||||
|
|
|
@ -73,7 +73,7 @@ fun View.hideKeyboard() {
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun View.addKeyboardInsetListener(lambda: (visible: Boolean) -> Unit) {
|
fun View.setKeyboardInsetListener(lambda: (visible: Boolean) -> Unit) {
|
||||||
doOnLayout {
|
doOnLayout {
|
||||||
var isKeyboardVisible = ViewCompat.getRootWindowInsets(this)?.isVisible(
|
var isKeyboardVisible = ViewCompat.getRootWindowInsets(this)?.isVisible(
|
||||||
WindowInsetsCompat.Type.ime()
|
WindowInsetsCompat.Type.ime()
|
||||||
|
@ -81,8 +81,9 @@ fun View.addKeyboardInsetListener(lambda: (visible: Boolean) -> Unit) {
|
||||||
|
|
||||||
lambda(isKeyboardVisible)
|
lambda(isKeyboardVisible)
|
||||||
|
|
||||||
|
// See https://issuetracker.google.com/issues/281942480
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(
|
ViewCompat.setOnApplyWindowInsetsListener(
|
||||||
this
|
rootView
|
||||||
) { view, insets ->
|
) { view, insets ->
|
||||||
val keyboardVisibilityChanged = ViewCompat.getRootWindowInsets(view)
|
val keyboardVisibilityChanged = ViewCompat.getRootWindowInsets(view)
|
||||||
?.isVisible(WindowInsetsCompat.Type.ime()) == true
|
?.isVisible(WindowInsetsCompat.Type.ime()) == true
|
||||||
|
@ -90,7 +91,7 @@ fun View.addKeyboardInsetListener(lambda: (visible: Boolean) -> Unit) {
|
||||||
isKeyboardVisible = keyboardVisibilityChanged
|
isKeyboardVisible = keyboardVisibilityChanged
|
||||||
lambda(isKeyboardVisible)
|
lambda(isKeyboardVisible)
|
||||||
}
|
}
|
||||||
insets
|
ViewCompat.onApplyWindowInsets(view, insets)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -343,7 +344,7 @@ fun setImageViewScaleType(imageView: ImageView, scaleType: ImageView.ScaleType)
|
||||||
|
|
||||||
@BindingAdapter("coilRounded")
|
@BindingAdapter("coilRounded")
|
||||||
fun loadRoundImageWithCoil(imageView: ImageView, path: String?) {
|
fun loadRoundImageWithCoil(imageView: ImageView, path: String?) {
|
||||||
if (path != null && path.isNotEmpty() && FileUtils.isExtensionImage(path)) {
|
if (!path.isNullOrEmpty() && FileUtils.isExtensionImage(path)) {
|
||||||
imageView.load(path) {
|
imageView.load(path) {
|
||||||
transformations(CircleCropTransformation())
|
transformations(CircleCropTransformation())
|
||||||
}
|
}
|
||||||
|
@ -354,7 +355,7 @@ fun loadRoundImageWithCoil(imageView: ImageView, path: String?) {
|
||||||
|
|
||||||
@BindingAdapter("coil")
|
@BindingAdapter("coil")
|
||||||
fun loadImageWithCoil(imageView: ImageView, path: String?) {
|
fun loadImageWithCoil(imageView: ImageView, path: String?) {
|
||||||
if (path != null && path.isNotEmpty() && FileUtils.isExtensionImage(path)) {
|
if (!path.isNullOrEmpty() && FileUtils.isExtensionImage(path)) {
|
||||||
if (corePreferences.vfsEnabled && path.startsWith(corePreferences.vfsCachePath)) {
|
if (corePreferences.vfsEnabled && path.startsWith(corePreferences.vfsCachePath)) {
|
||||||
imageView.load(path) {
|
imageView.load(path) {
|
||||||
diskCachePolicy(CachePolicy.DISABLED)
|
diskCachePolicy(CachePolicy.DISABLED)
|
||||||
|
@ -551,9 +552,23 @@ fun loadAvatarWithCoil(imageView: ImageView, path: String?) {
|
||||||
|
|
||||||
@BindingAdapter("coilVideoPreview")
|
@BindingAdapter("coilVideoPreview")
|
||||||
fun loadVideoPreview(imageView: ImageView, path: String?) {
|
fun loadVideoPreview(imageView: ImageView, path: String?) {
|
||||||
if (path != null && path.isNotEmpty() && FileUtils.isExtensionVideo(path)) {
|
if (!path.isNullOrEmpty() && FileUtils.isExtensionVideo(path)) {
|
||||||
imageView.load(path) {
|
imageView.load(path) {
|
||||||
videoFrameMillis(0)
|
videoFrameMillis(0)
|
||||||
|
listener(
|
||||||
|
onError = { _, result ->
|
||||||
|
Log.e(
|
||||||
|
"[Data Binding] [Coil] Error getting preview picture from video? [$path]: ${result.throwable}"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onSuccess = { _, _ ->
|
||||||
|
// Display "play" button above video preview
|
||||||
|
LayoutInflater.from(imageView.context).inflate(
|
||||||
|
R.layout.video_play_button,
|
||||||
|
imageView.parent as ViewGroup
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -582,13 +597,23 @@ fun addPhoneNumberEditTextValidation(editText: EditText, enabled: Boolean) {
|
||||||
fun addPrefixEditTextValidation(editText: EditText, enabled: Boolean) {
|
fun addPrefixEditTextValidation(editText: EditText, enabled: Boolean) {
|
||||||
if (!enabled) return
|
if (!enabled) return
|
||||||
editText.addTextChangedListener(object : TextWatcher {
|
editText.addTextChangedListener(object : TextWatcher {
|
||||||
override fun afterTextChanged(s: Editable?) {}
|
override fun afterTextChanged(s: Editable?) {
|
||||||
|
val dialPlan = PhoneNumberUtils.getDialPlanFromCountryCallingPrefix(
|
||||||
|
s.toString().substring(1)
|
||||||
|
)
|
||||||
|
if (dialPlan == null) {
|
||||||
|
editText.error =
|
||||||
|
editText.context.getString(
|
||||||
|
R.string.assistant_error_invalid_international_prefix
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||||
if (s == null || s.isEmpty() || !s.startsWith("+")) {
|
if (s.isNullOrEmpty() || !s.startsWith("+")) {
|
||||||
editText.setText("+$s")
|
editText.setText("+$s")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,10 @@ import android.content.Intent
|
||||||
import android.database.CursorIndexOutOfBoundsException
|
import android.database.CursorIndexOutOfBoundsException
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.os.Process.myUid
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
|
import android.system.Os.fstat
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import java.io.*
|
import java.io.*
|
||||||
|
@ -42,6 +45,15 @@ import org.linphone.R
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
|
|
||||||
class FileUtils {
|
class FileUtils {
|
||||||
|
enum class MimeType {
|
||||||
|
PlainText,
|
||||||
|
Pdf,
|
||||||
|
Image,
|
||||||
|
Video,
|
||||||
|
Audio,
|
||||||
|
Unknown
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getNameFromFilePath(filePath: String): String {
|
fun getNameFromFilePath(filePath: String): String {
|
||||||
var name = filePath
|
var name = filePath
|
||||||
|
@ -64,36 +76,28 @@ class FileUtils {
|
||||||
return extension.lowercase(Locale.getDefault())
|
return extension.lowercase(Locale.getDefault())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isMimePlainText(type: String?): Boolean {
|
fun getMimeType(type: String?): MimeType {
|
||||||
return type?.startsWith("text/plain") ?: false
|
if (type.isNullOrEmpty()) return MimeType.Unknown
|
||||||
|
return when {
|
||||||
|
type.startsWith("image/") -> MimeType.Image
|
||||||
|
type.startsWith("text/plain") -> MimeType.PlainText
|
||||||
|
type.startsWith("video/") -> MimeType.Video
|
||||||
|
type.startsWith("audio/") -> MimeType.Audio
|
||||||
|
type.startsWith("application/pdf") -> MimeType.Pdf
|
||||||
|
else -> MimeType.Unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isMimePdf(type: String?): Boolean {
|
|
||||||
return type?.startsWith("application/pdf") ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isMimeImage(type: String?): Boolean {
|
|
||||||
return type?.startsWith("image/") ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isMimeVideo(type: String?): Boolean {
|
|
||||||
return type?.startsWith("video/") ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isMimeAudio(type: String?): Boolean {
|
|
||||||
return type?.startsWith("audio/") ?: false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isExtensionImage(path: String): Boolean {
|
fun isExtensionImage(path: String): Boolean {
|
||||||
val extension = getExtensionFromFileName(path)
|
val extension = getExtensionFromFileName(path)
|
||||||
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||||
return isMimeImage(type)
|
return getMimeType(type) == MimeType.Image
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isExtensionVideo(path: String): Boolean {
|
fun isExtensionVideo(path: String): Boolean {
|
||||||
val extension = getExtensionFromFileName(path)
|
val extension = getExtensionFromFileName(path)
|
||||||
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||||
return isMimeVideo(type)
|
return getMimeType(type) == MimeType.Video
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearExistingPlainFiles() {
|
fun clearExistingPlainFiles() {
|
||||||
|
@ -269,6 +273,21 @@ class FileUtils {
|
||||||
var result: String? = null
|
var result: String? = null
|
||||||
val name: String = getNameFromUri(uri, context)
|
val name: String = getNameFromUri(uri, context)
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fstat(
|
||||||
|
ParcelFileDescriptor.open(
|
||||||
|
File(uri.path),
|
||||||
|
ParcelFileDescriptor.MODE_READ_ONLY
|
||||||
|
).fileDescriptor
|
||||||
|
).st_uid != myUid()
|
||||||
|
) {
|
||||||
|
Log.e("[File Utils] File descriptor UID different from our, denying copy!")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("[File Utils] Can't check file ownership: ", e)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val localFile: File = createFile(name)
|
val localFile: File = createFile(name)
|
||||||
val remoteFile =
|
val remoteFile =
|
||||||
|
|
|
@ -51,7 +51,7 @@ class LinphoneUtils {
|
||||||
}
|
}
|
||||||
val localDisplayName = account?.params?.identityAddress?.displayName
|
val localDisplayName = account?.params?.identityAddress?.displayName
|
||||||
// Do not return an empty local display name
|
// Do not return an empty local display name
|
||||||
if (localDisplayName != null && localDisplayName.isNotEmpty()) {
|
if (!localDisplayName.isNullOrEmpty()) {
|
||||||
return localDisplayName
|
return localDisplayName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,7 +179,7 @@ class LinphoneUtils {
|
||||||
fun deleteFilesAttachedToChatMessage(chatMessage: ChatMessage) {
|
fun deleteFilesAttachedToChatMessage(chatMessage: ChatMessage) {
|
||||||
for (content in chatMessage.contents) {
|
for (content in chatMessage.contents) {
|
||||||
val filePath = content.filePath
|
val filePath = content.filePath
|
||||||
if (filePath != null && filePath.isNotEmpty()) {
|
if (!filePath.isNullOrEmpty()) {
|
||||||
Log.i("[Linphone Utils] Deleting file $filePath")
|
Log.i("[Linphone Utils] Deleting file $filePath")
|
||||||
FileUtils.deleteFile(filePath)
|
FileUtils.deleteFile(filePath)
|
||||||
}
|
}
|
||||||
|
@ -288,6 +288,11 @@ class LinphoneUtils {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isFileTransferAvailable(): Boolean {
|
||||||
|
val core = coreContext.core
|
||||||
|
return core.fileTransferServer.orEmpty().isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
fun hashPassword(
|
fun hashPassword(
|
||||||
userId: String,
|
userId: String,
|
||||||
password: String,
|
password: String,
|
||||||
|
|
|
@ -181,13 +181,13 @@
|
||||||
android:contentDescription="@null"
|
android:contentDescription="@null"
|
||||||
coilVoipContact="@{conferenceViewModel.speakingParticipant}"
|
coilVoipContact="@{conferenceViewModel.speakingParticipant}"
|
||||||
android:background="@drawable/generated_avatar_bg"
|
android:background="@drawable/generated_avatar_bg"
|
||||||
app:layout_constraintDimensionRatio="1:1"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/active_speaker_background"
|
app:layout_constraintBottom_toBottomOf="@id/active_speaker_background"
|
||||||
app:layout_constraintEnd_toEndOf="@id/active_speaker_background"
|
app:layout_constraintEnd_toEndOf="@id/active_speaker_background"
|
||||||
app:layout_constraintHeight_max="@dimen/voip_contact_avatar_max_size"
|
app:layout_constraintHeight_max="@dimen/voip_contact_avatar_max_size"
|
||||||
app:layout_constraintStart_toStartOf="@id/active_speaker_background"
|
app:layout_constraintStart_toStartOf="@id/active_speaker_background"
|
||||||
app:layout_constraintTop_toTopOf="@id/active_speaker_background"
|
app:layout_constraintTop_toTopOf="@id/active_speaker_background"
|
||||||
app:layout_constraintWidth_max="@dimen/voip_contact_avatar_max_size" />
|
app:layout_constraintWidth_max="@dimen/voip_contact_avatar_max_size"
|
||||||
|
app:layout_constraintDimensionRatio="1:1" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/speaking_participant_paused"
|
android:id="@+id/speaking_participant_paused"
|
||||||
|
|
|
@ -119,6 +119,7 @@
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
assistantPhoneNumberPrefixValidation="@{true}"
|
assistantPhoneNumberPrefixValidation="@{true}"
|
||||||
|
errorMessage="@={viewModel.prefixError}"
|
||||||
android:text="@={viewModel.prefix}"
|
android:text="@={viewModel.prefix}"
|
||||||
android:imeOptions="actionNext"
|
android:imeOptions="actionNext"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
|
|
|
@ -4,8 +4,7 @@
|
||||||
android:layout_height="40dp"
|
android:layout_height="40dp"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:padding="5dp"
|
android:padding="5dp">
|
||||||
android:background="?attr/backgroundColor">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/country_name"
|
android:id="@+id/country_name"
|
||||||
|
|
|
@ -116,6 +116,7 @@
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
assistantPhoneNumberPrefixValidation="@{true}"
|
assistantPhoneNumberPrefixValidation="@{true}"
|
||||||
|
errorMessage="@={viewModel.prefixError}"
|
||||||
android:text="@={viewModel.prefix}"
|
android:text="@={viewModel.prefix}"
|
||||||
android:imeOptions="actionNext"
|
android:imeOptions="actionNext"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
|
|
|
@ -126,6 +126,7 @@
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
assistantPhoneNumberPrefixValidation="@{true}"
|
assistantPhoneNumberPrefixValidation="@{true}"
|
||||||
|
errorMessage="@={viewModel.prefixError}"
|
||||||
android:text="@={viewModel.prefix}"
|
android:text="@={viewModel.prefix}"
|
||||||
android:imeOptions="actionNext"
|
android:imeOptions="actionNext"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
<layout xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<data>
|
<data>
|
||||||
<import type="android.view.View"/>
|
<import type="android.view.View"/>
|
||||||
|
@ -14,8 +16,15 @@
|
||||||
android:layout_margin="5dp"
|
android:layout_margin="5dp"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:background="?attr/backgroundColor"/>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:visibility="@{data.image ? View.VISIBLE : View.GONE}"
|
android:visibility="@{data.image ? View.VISIBLE : View.GONE, default=gone}"
|
||||||
android:contentDescription="@string/content_description_pending_file_transfer"
|
android:contentDescription="@string/content_description_pending_file_transfer"
|
||||||
android:layout_width="100dp"
|
android:layout_width="100dp"
|
||||||
android:layout_height="100dp"
|
android:layout_height="100dp"
|
||||||
|
@ -25,8 +34,25 @@
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
coil="@{data.path}"/>
|
coil="@{data.path}"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:visibility="@{data.video ? View.VISIBLE : View.GONE, default=gone}"
|
||||||
|
style="@style/chat_file_attachment_font"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:layout_marginBottom="5dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
android:ellipsize="middle"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:drawablePadding="5dp"
|
||||||
|
android:text="@{data.fileName, default=`test.mkv`}"
|
||||||
|
app:drawableTopCompat="@drawable/file_video" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:visibility="@{data.video ? View.VISIBLE : View.GONE}"
|
android:visibility="@{data.video ? View.VISIBLE : View.GONE, default=gone}"
|
||||||
android:contentDescription="@string/content_description_pending_file_transfer"
|
android:contentDescription="@string/content_description_pending_file_transfer"
|
||||||
android:layout_width="100dp"
|
android:layout_width="100dp"
|
||||||
android:layout_height="100dp"
|
android:layout_height="100dp"
|
||||||
|
@ -36,41 +62,23 @@
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
coilVideoPreview="@{data.path}"/>
|
coilVideoPreview="@{data.path}"/>
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:visibility="@{data.video ? View.VISIBLE : View.GONE, default=gone}"
|
|
||||||
android:layout_width="@dimen/play_pause_button_size"
|
|
||||||
android:layout_height="@dimen/play_pause_button_size"
|
|
||||||
android:padding="9dp"
|
|
||||||
android:src="@drawable/record_play_dark"
|
|
||||||
android:background="@drawable/round_recording_button_background_dark"
|
|
||||||
android:contentDescription="@string/content_description_chat_message_video_attachment"
|
|
||||||
android:layout_centerInParent="true"/>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="100dp"
|
|
||||||
android:layout_height="100dp"
|
|
||||||
android:layout_alignParentBottom="true"
|
|
||||||
android:layout_alignParentLeft="true"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:gravity="center"
|
|
||||||
android:background="?attr/backgroundColor"
|
|
||||||
android:visibility="@{data.image || data.video ? View.GONE : View.VISIBLE}">
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
style="@style/chat_file_attachment_font"
|
style="@style/chat_file_attachment_font"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="5dp"
|
android:layout_alignParentBottom="true"
|
||||||
|
android:layout_marginBottom="5dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
android:ellipsize="middle"
|
android:ellipsize="middle"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
android:drawablePadding="5dp"
|
android:drawablePadding="5dp"
|
||||||
|
android:visibility="@{data.image || data.video ? View.GONE : View.VISIBLE, default=gone}"
|
||||||
|
android:text="@{data.fileName, default=`test.txt`}"
|
||||||
android:drawableTop="@{data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : @drawable/file), default=@drawable/file}"
|
android:drawableTop="@{data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : @drawable/file), default=@drawable/file}"
|
||||||
android:text="@{data.fileName, default=`test.txt`}"/>
|
tools:ignore="UseCompatTextViewDrawableXml" />
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:onClick="@{() -> data.delete()}"
|
android:onClick="@{() -> data.delete()}"
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
<layout xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<data>
|
<data>
|
||||||
<import type="android.view.View" />
|
<import type="android.view.View" />
|
||||||
|
@ -29,7 +31,6 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="5dp"
|
android:layout_margin="5dp"
|
||||||
android:drawableTop="@{data.video ? @drawable/file_video : (data.image ? @drawable/file_picture : (data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : (data.voiceRecording ? @drawable/audio_recording_reply_preview_default : @drawable/file)))), default=@drawable/file}"
|
|
||||||
android:drawablePadding="5dp"
|
android:drawablePadding="5dp"
|
||||||
android:ellipsize="middle"
|
android:ellipsize="middle"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
|
@ -37,7 +38,9 @@
|
||||||
android:onLongClick="@{longClickListener}"
|
android:onLongClick="@{longClickListener}"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:text="@{data.fileName, default=`test.pdf`}"
|
android:text="@{data.fileName, default=`test.pdf`}"
|
||||||
android:textAlignment="center" />
|
android:textAlignment="center"
|
||||||
|
android:drawableTop="@{data.video ? @drawable/file_video : (data.image ? @drawable/file_picture : (data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : (data.voiceRecording ? @drawable/audio_recording_reply_preview_default : @drawable/file)))), default=@drawable/file}"
|
||||||
|
tools:ignore="UseCompatTextViewDrawableXml" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
style="@style/chat_file_attachment_font"
|
style="@style/chat_file_attachment_font"
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
<layout xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<data>
|
<data>
|
||||||
<import type="android.view.View" />
|
<import type="android.view.View" />
|
||||||
|
@ -15,6 +17,8 @@
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
|
android:onClick="@{() -> data.openFile()}"
|
||||||
|
android:onLongClick="@{longClickListener}"
|
||||||
android:layout_width="@dimen/chat_message_bubble_file_size"
|
android:layout_width="@dimen/chat_message_bubble_file_size"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:minHeight="@dimen/chat_message_bubble_file_size"
|
android:minHeight="@dimen/chat_message_bubble_file_size"
|
||||||
|
@ -29,15 +33,14 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="5dp"
|
android:layout_margin="5dp"
|
||||||
android:drawableTop="@{data.video ? @drawable/file_video : (data.image ? @drawable/file_picture : (data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : (data.voiceRecording ? @drawable/audio_recording_reply_preview_default : @drawable/file)))), default=@drawable/file}"
|
|
||||||
android:drawablePadding="5dp"
|
android:drawablePadding="5dp"
|
||||||
android:ellipsize="middle"
|
android:ellipsize="middle"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:onClick="@{() -> data.openFile()}"
|
|
||||||
android:onLongClick="@{longClickListener}"
|
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:text="@{data.fileName, default=`test.pdf`}"
|
android:text="@{data.fileName, default=`test.pdf`}"
|
||||||
android:textAlignment="center" />
|
android:textAlignment="center"
|
||||||
|
android:drawableTop="@{data.video ? @drawable/file_video : (data.image ? @drawable/file_picture : (data.pdf ? @drawable/file_pdf : (data.audio ? @drawable/file_audio : (data.voiceRecording ? @drawable/audio_recording_reply_preview_default : @drawable/file)))), default=@drawable/file}"
|
||||||
|
tools:ignore="UseCompatTextViewDrawableXml" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
|
|
|
@ -18,14 +18,14 @@
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:onClick="@{() -> data.openFile()}"
|
||||||
|
android:onLongClick="@{longClickListener}"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:visibility="@{inflatedVisibility}"
|
android:visibility="@{inflatedVisibility}"
|
||||||
inflatedLifecycleOwner="@{true}">
|
inflatedLifecycleOwner="@{true}">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:onClick="@{() -> data.openFile()}"
|
|
||||||
android:onLongClick="@{longClickListener}"
|
|
||||||
android:contentDescription="@string/content_description_downloaded_file_transfer"
|
android:contentDescription="@string/content_description_downloaded_file_transfer"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -85,7 +85,8 @@
|
||||||
android:background="@drawable/led_background"
|
android:background="@drawable/led_background"
|
||||||
android:padding="2dp"
|
android:padding="2dp"
|
||||||
app:presenceIcon="@{data.presenceStatus}"
|
app:presenceIcon="@{data.presenceStatus}"
|
||||||
android:visibility="@{data.chatMessage.outgoing || selectionListViewModel.isEditionEnabled || data.hideAvatar || data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" />
|
android:visibility="@{data.chatMessage.outgoing || selectionListViewModel.isEditionEnabled || data.hideAvatar || data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/background"
|
android:id="@+id/background"
|
||||||
|
|
|
@ -26,6 +26,15 @@
|
||||||
android:scaleType="@{ScaleType.CENTER_CROP}"
|
android:scaleType="@{ScaleType.CENTER_CROP}"
|
||||||
android:adjustViewBounds="true" />
|
android:adjustViewBounds="true" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="@dimen/chat_message_small_bubble_file_size"
|
||||||
|
android:layout_height="@dimen/chat_message_small_bubble_file_size"
|
||||||
|
android:background="@drawable/chat_bubble_reply_file_background"
|
||||||
|
android:contentDescription="@{data.fileName}"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:src="@drawable/file_video"
|
||||||
|
android:visibility="@{data.video ? View.VISIBLE : View.GONE, default=gone}" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:contentDescription="@string/content_description_downloaded_file_transfer"
|
android:contentDescription="@string/content_description_downloaded_file_transfer"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -37,16 +46,6 @@
|
||||||
android:scaleType="@{ScaleType.CENTER_CROP}"
|
android:scaleType="@{ScaleType.CENTER_CROP}"
|
||||||
android:adjustViewBounds="true" />
|
android:adjustViewBounds="true" />
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:visibility="@{data.video ? View.VISIBLE : View.GONE, default=gone}"
|
|
||||||
android:layout_width="@dimen/play_pause_button_size"
|
|
||||||
android:layout_height="@dimen/play_pause_button_size"
|
|
||||||
android:padding="9dp"
|
|
||||||
android:src="@drawable/record_play_dark"
|
|
||||||
android:background="@drawable/round_recording_button_background_dark"
|
|
||||||
android:contentDescription="@string/content_description_chat_message_video_attachment"
|
|
||||||
android:layout_centerInParent="true"/>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:visibility="@{data.isVoiceRecording ? View.VISIBLE : View.GONE, default=gone}"
|
android:visibility="@{data.isVoiceRecording ? View.VISIBLE : View.GONE, default=gone}"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
|
@ -24,6 +24,16 @@
|
||||||
android:scaleType="@{ScaleType.CENTER_CROP}"
|
android:scaleType="@{ScaleType.CENTER_CROP}"
|
||||||
android:adjustViewBounds="true" />
|
android:adjustViewBounds="true" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="@dimen/chat_message_small_bubble_file_size"
|
||||||
|
android:layout_height="@dimen/chat_message_small_bubble_file_size"
|
||||||
|
android:layout_margin="5dp"
|
||||||
|
android:background="@drawable/chat_bubble_reply_file_background"
|
||||||
|
android:contentDescription="@{data.fileName}"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:src="@drawable/file_video"
|
||||||
|
android:visibility="@{data.video ? View.VISIBLE : View.GONE, default=gone}" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:contentDescription="@string/content_description_downloaded_file_transfer"
|
android:contentDescription="@string/content_description_downloaded_file_transfer"
|
||||||
android:layout_width="@dimen/chat_message_small_bubble_file_size"
|
android:layout_width="@dimen/chat_message_small_bubble_file_size"
|
||||||
|
@ -34,17 +44,6 @@
|
||||||
android:scaleType="@{ScaleType.CENTER_CROP}"
|
android:scaleType="@{ScaleType.CENTER_CROP}"
|
||||||
android:adjustViewBounds="true" />
|
android:adjustViewBounds="true" />
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:visibility="@{data.video ? View.VISIBLE : View.GONE, default=gone}"
|
|
||||||
android:layout_width="@dimen/play_pause_button_size"
|
|
||||||
android:layout_height="@dimen/play_pause_button_size"
|
|
||||||
android:layout_margin="5dp"
|
|
||||||
android:padding="9dp"
|
|
||||||
android:src="@drawable/record_play_dark"
|
|
||||||
android:background="@drawable/round_recording_button_background_dark"
|
|
||||||
android:contentDescription="@string/content_description_chat_message_video_attachment"
|
|
||||||
android:layout_centerInParent="true"/>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:visibility="@{data.isVoiceRecording ? View.VISIBLE : View.GONE, default=gone}"
|
android:visibility="@{data.isVoiceRecording ? View.VISIBLE : View.GONE, default=gone}"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
@ -74,10 +73,7 @@
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="@dimen/chat_message_small_bubble_file_size"
|
android:layout_width="@dimen/chat_message_small_bubble_file_size"
|
||||||
android:layout_height="@dimen/chat_message_small_bubble_file_size"
|
android:layout_height="@dimen/chat_message_small_bubble_file_size"
|
||||||
android:layout_marginStart="5dp"
|
android:layout_margin="5dp"
|
||||||
android:layout_marginTop="5dp"
|
|
||||||
android:layout_marginEnd="5dp"
|
|
||||||
android:layout_marginBottom="5dp"
|
|
||||||
android:background="@drawable/chat_bubble_reply_file_background"
|
android:background="@drawable/chat_bubble_reply_file_background"
|
||||||
android:contentDescription="@{data.fileName}"
|
android:contentDescription="@{data.fileName}"
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android">
|
<layout xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<data>
|
<data>
|
||||||
<import type="android.view.View" />
|
<import type="android.view.View" />
|
||||||
|
@ -16,14 +17,29 @@
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
|
android:onClick="@{() -> data.openFile()}"
|
||||||
|
android:onLongClick="@{longClickListener}"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:visibility="@{inflatedVisibility}"
|
android:visibility="@{inflatedVisibility}"
|
||||||
inflatedLifecycleOwner="@{true}">
|
inflatedLifecycleOwner="@{true}"
|
||||||
|
android:background="?attr/backgroundColor">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/chat_file_attachment_font"
|
||||||
|
android:layout_width="100dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:ellipsize="middle"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:drawablePadding="5dp"
|
||||||
|
android:text="@{data.fileName, default=`test.mkv`}"
|
||||||
|
app:drawableTopCompat="@drawable/file_video" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:onClick="@{() -> data.openFile()}"
|
|
||||||
android:onLongClick="@{longClickListener}"
|
|
||||||
android:contentDescription="@string/content_description_downloaded_file_transfer"
|
android:contentDescription="@string/content_description_downloaded_file_transfer"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -33,15 +49,6 @@
|
||||||
android:scaleType="@{data.alone ? ScaleType.FIT_CENTER : ScaleType.CENTER_CROP}"
|
android:scaleType="@{data.alone ? ScaleType.FIT_CENTER : ScaleType.CENTER_CROP}"
|
||||||
android:adjustViewBounds="true" />
|
android:adjustViewBounds="true" />
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:layout_width="@dimen/play_pause_button_size"
|
|
||||||
android:layout_height="@dimen/play_pause_button_size"
|
|
||||||
android:padding="9dp"
|
|
||||||
android:src="@drawable/record_play_dark"
|
|
||||||
android:background="@drawable/round_recording_button_background_dark"
|
|
||||||
android:contentDescription="@string/content_description_chat_message_video_attachment"
|
|
||||||
android:layout_centerInParent="true"/>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
</layout>
|
</layout>
|
|
@ -163,6 +163,7 @@
|
||||||
android:layout_alignRight="@id/searchBar"
|
android:layout_alignRight="@id/searchBar"
|
||||||
android:layout_alignBottom="@id/searchBar"
|
android:layout_alignBottom="@id/searchBar"
|
||||||
android:onClick="@{() -> viewModel.clearFilter()}"
|
android:onClick="@{() -> viewModel.clearFilter()}"
|
||||||
|
android:contentDescription="@string/content_description_clear_field"
|
||||||
android:src="@drawable/field_clean" />
|
android:src="@drawable/field_clean" />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/backgroundColor"
|
||||||
android:clickable="true">
|
android:clickable="true">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<data>
|
<data>
|
||||||
<import type="android.view.View"/>
|
<import type="android.view.View"/>
|
||||||
|
@ -49,7 +50,8 @@
|
||||||
android:background="@drawable/led_background"
|
android:background="@drawable/led_background"
|
||||||
android:padding="@dimen/contact_presence_badge_padding"
|
android:padding="@dimen/contact_presence_badge_padding"
|
||||||
app:presenceIcon="@{data.presenceStatus}"
|
app:presenceIcon="@{data.presenceStatus}"
|
||||||
android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" />
|
android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="20dp"
|
android:layout_width="20dp"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<data>
|
<data>
|
||||||
<import type="android.view.View"/>
|
<import type="android.view.View"/>
|
||||||
|
@ -50,7 +51,8 @@
|
||||||
android:background="@drawable/led_background"
|
android:background="@drawable/led_background"
|
||||||
android:padding="@dimen/contact_presence_badge_padding"
|
android:padding="@dimen/contact_presence_badge_padding"
|
||||||
android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}"
|
android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}"
|
||||||
app:presenceIcon="@{data.presenceStatus}" />
|
app:presenceIcon="@{data.presenceStatus}"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_width="20dp"
|
android:layout_width="20dp"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<data>
|
<data>
|
||||||
<import type="android.view.View"/>
|
<import type="android.view.View"/>
|
||||||
|
@ -37,7 +38,8 @@
|
||||||
android:background="@drawable/led_background"
|
android:background="@drawable/led_background"
|
||||||
android:padding="@dimen/contact_presence_badge_padding"
|
android:padding="@dimen/contact_presence_badge_padding"
|
||||||
app:presenceIcon="@{data.presenceStatus}"
|
app:presenceIcon="@{data.presenceStatus}"
|
||||||
android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" />
|
android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/time"
|
android:id="@+id/time"
|
||||||
|
|
|
@ -62,7 +62,8 @@
|
||||||
app:presenceIcon="@{data.presenceStatus}"
|
app:presenceIcon="@{data.presenceStatus}"
|
||||||
android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}"
|
android:visibility="@{data.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}"
|
||||||
app:layout_constraintStart_toStartOf="@id/avatar"
|
app:layout_constraintStart_toStartOf="@id/avatar"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/avatar"/>
|
app:layout_constraintBottom_toBottomOf="@id/avatar"
|
||||||
|
tools:ignore="ContentDescription"/>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/securityLevel"
|
android:id="@+id/securityLevel"
|
||||||
|
|
|
@ -34,7 +34,8 @@
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:layout_width="@dimen/master_fragment_width"
|
android:layout_width="@dimen/master_fragment_width"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="start">
|
android:layout_gravity="start"
|
||||||
|
android:background="?attr/backgroundColor">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/top_bar"
|
android:id="@+id/top_bar"
|
||||||
|
|
|
@ -125,9 +125,8 @@
|
||||||
android:layout_width="@dimen/chat_message_sending_icons_size"
|
android:layout_width="@dimen/chat_message_sending_icons_size"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginStart="@dimen/chat_message_sending_icons_margin"
|
android:layout_marginStart="@dimen/chat_message_sending_icons_margin"
|
||||||
android:layout_marginEnd="@dimen/chat_message_sending_icons_margin"
|
|
||||||
android:contentDescription="@string/content_description_attach_file"
|
android:contentDescription="@string/content_description_attach_file"
|
||||||
android:enabled="@{chatSendingViewModel.attachFileEnabled && !chatSendingViewModel.attachFilePending}"
|
android:enabled="@{chatSendingViewModel.attachFileEnabled && !chatSendingViewModel.attachFilePending && chatSendingViewModel.isFileTransferAvailable}"
|
||||||
android:onClick="@{attachFileClickListener}"
|
android:onClick="@{attachFileClickListener}"
|
||||||
android:paddingTop="@dimen/chat_message_sending_icons_margin"
|
android:paddingTop="@dimen/chat_message_sending_icons_margin"
|
||||||
android:paddingBottom="@dimen/chat_message_sending_icons_margin"
|
android:paddingBottom="@dimen/chat_message_sending_icons_margin"
|
||||||
|
@ -142,13 +141,13 @@
|
||||||
android:layout_width="@dimen/chat_message_sending_icons_size"
|
android:layout_width="@dimen/chat_message_sending_icons_size"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginStart="@dimen/chat_message_sending_icons_margin"
|
android:layout_marginStart="@dimen/chat_message_sending_icons_margin"
|
||||||
android:layout_marginEnd="@dimen/chat_message_sending_icons_margin"
|
|
||||||
android:contentDescription="@string/content_description_voice_recording"
|
android:contentDescription="@string/content_description_voice_recording"
|
||||||
android:onClick="@{() -> chatSendingViewModel.toggleVoiceRecording()}"
|
android:onClick="@{() -> chatSendingViewModel.toggleVoiceRecording()}"
|
||||||
android:onTouch="@{voiceRecordingTouchListener}"
|
android:onTouch="@{voiceRecordingTouchListener}"
|
||||||
android:paddingTop="@dimen/chat_message_sending_icons_margin"
|
android:paddingTop="@dimen/chat_message_sending_icons_margin"
|
||||||
android:paddingBottom="@dimen/chat_message_sending_icons_margin"
|
android:paddingBottom="@dimen/chat_message_sending_icons_margin"
|
||||||
android:selected="@{chatSendingViewModel.isVoiceRecording}"
|
android:selected="@{chatSendingViewModel.isVoiceRecording}"
|
||||||
|
android:enabled="@{chatSendingViewModel.isFileTransferAvailable}"
|
||||||
android:src="@drawable/record_audio_message"
|
android:src="@drawable/record_audio_message"
|
||||||
app:layout_constraintHeight_max="@dimen/chat_message_sending_icons_size"
|
app:layout_constraintHeight_max="@dimen/chat_message_sending_icons_size"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/message"
|
app:layout_constraintBottom_toBottomOf="@id/message"
|
||||||
|
@ -162,6 +161,7 @@
|
||||||
android:layout_below="@id/emoji_picker"
|
android:layout_below="@id/emoji_picker"
|
||||||
android:layout_gravity="center_vertical"
|
android:layout_gravity="center_vertical"
|
||||||
android:layout_marginStart="@dimen/chat_message_sending_icons_margin"
|
android:layout_marginStart="@dimen/chat_message_sending_icons_margin"
|
||||||
|
android:layout_marginEnd="@{chatSendingViewModel.isEmojiPickerVisible ? @dimen/margin_0dp : @dimen/chat_message_sending_icons_margin}"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
android:layout_marginBottom="10dp"
|
android:layout_marginBottom="10dp"
|
||||||
android:background="@color/header_background_color"
|
android:background="@color/header_background_color"
|
||||||
|
@ -174,37 +174,21 @@
|
||||||
android:textColor="@color/black_color"
|
android:textColor="@color/black_color"
|
||||||
android:textCursorDrawable="@null"
|
android:textCursorDrawable="@null"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toStartOf="@id/message_right_barrier"
|
app:layout_constraintEnd_toStartOf="@id/emoji_picker_toggle"
|
||||||
app:layout_constraintStart_toEndOf="@id/voice_record"
|
app:layout_constraintStart_toEndOf="@id/voice_record"
|
||||||
app:layout_constraintTop_toBottomOf="@id/emoji_picker" />
|
app:layout_constraintTop_toBottomOf="@id/emoji_picker" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
|
||||||
android:id="@+id/message_right_barrier"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:barrierDirection="left"
|
|
||||||
app:constraint_referenced_ids="send_message, emoji_picker_toggle"/>
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/emoji_picker_bg"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:background="@color/header_background_color"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/message"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/emoji_picker_toggle"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/emoji_picker_toggle"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/message" />
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/emoji_picker_toggle"
|
android:id="@+id/emoji_picker_toggle"
|
||||||
android:layout_width="@dimen/chat_message_sending_icons_size"
|
android:layout_width="@dimen/chat_message_sending_icons_size"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginStart="@dimen/chat_message_sending_icons_margin"
|
|
||||||
android:layout_marginEnd="@dimen/chat_message_sending_icons_margin"
|
android:layout_marginEnd="@dimen/chat_message_sending_icons_margin"
|
||||||
android:onClick="@{() -> chatSendingViewModel.toggleEmojiPicker()}"
|
android:onClick="@{() -> chatSendingViewModel.toggleEmojiPicker()}"
|
||||||
android:paddingTop="@dimen/chat_message_sending_icons_margin"
|
android:paddingTop="@dimen/chat_message_sending_icons_margin"
|
||||||
android:paddingBottom="@dimen/chat_message_sending_icons_margin"
|
android:paddingBottom="@dimen/chat_message_sending_icons_margin"
|
||||||
android:src="@drawable/emoji"
|
android:src="@drawable/emoji"
|
||||||
|
android:background="@color/header_background_color"
|
||||||
|
android:contentDescription="@string/content_description_emoji_picker"
|
||||||
android:selected="@{chatSendingViewModel.isEmojiPickerOpen}"
|
android:selected="@{chatSendingViewModel.isEmojiPickerOpen}"
|
||||||
android:visibility="@{chatSendingViewModel.isEmojiPickerVisible ? View.VISIBLE : View.GONE}"
|
android:visibility="@{chatSendingViewModel.isEmojiPickerVisible ? View.VISIBLE : View.GONE}"
|
||||||
app:layout_constraintHeight_max="@dimen/chat_message_sending_icons_size"
|
app:layout_constraintHeight_max="@dimen/chat_message_sending_icons_size"
|
||||||
|
@ -216,7 +200,6 @@
|
||||||
android:id="@+id/send_message"
|
android:id="@+id/send_message"
|
||||||
android:layout_width="@dimen/chat_message_sending_icons_size"
|
android:layout_width="@dimen/chat_message_sending_icons_size"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginStart="@dimen/chat_message_sending_icons_margin"
|
|
||||||
android:layout_marginEnd="@dimen/chat_message_sending_icons_margin"
|
android:layout_marginEnd="@dimen/chat_message_sending_icons_margin"
|
||||||
android:contentDescription="@string/content_description_send_message"
|
android:contentDescription="@string/content_description_send_message"
|
||||||
android:enabled="@{chatSendingViewModel.sendMessageEnabled && !chatSendingViewModel.attachingFileInProgress}"
|
android:enabled="@{chatSendingViewModel.sendMessageEnabled && !chatSendingViewModel.attachingFileInProgress}"
|
||||||
|
|
|
@ -79,6 +79,7 @@
|
||||||
android:layout_alignRight="@id/searchBar"
|
android:layout_alignRight="@id/searchBar"
|
||||||
android:layout_alignBottom="@id/searchBar"
|
android:layout_alignBottom="@id/searchBar"
|
||||||
android:onClick="@{() -> viewModel.clearFilter()}"
|
android:onClick="@{() -> viewModel.clearFilter()}"
|
||||||
|
android:contentDescription="@string/content_description_clear_field"
|
||||||
android:src="@drawable/field_clean" />
|
android:src="@drawable/field_clean" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
|
@ -102,6 +102,7 @@
|
||||||
android:layout_marginTop="5dp"
|
android:layout_marginTop="5dp"
|
||||||
android:layout_marginStart="5dp"
|
android:layout_marginStart="5dp"
|
||||||
android:layout_marginEnd="5dp"
|
android:layout_marginEnd="5dp"
|
||||||
|
android:inputType="textEmailSubject"
|
||||||
style="@style/conference_scheduling_font"
|
style="@style/conference_scheduling_font"
|
||||||
android:background="?attr/voipFormDisabledFieldBackgroundColor"
|
android:background="?attr/voipFormDisabledFieldBackgroundColor"
|
||||||
android:text="@{viewModel.subject}"
|
android:text="@{viewModel.subject}"
|
||||||
|
@ -252,6 +253,7 @@
|
||||||
android:gravity="top"
|
android:gravity="top"
|
||||||
android:minLines="3"
|
android:minLines="3"
|
||||||
android:maxLines="5"
|
android:maxLines="5"
|
||||||
|
android:inputType="textMultiLine"
|
||||||
style="@style/conference_scheduling_font"
|
style="@style/conference_scheduling_font"
|
||||||
android:background="?attr/voipFormDisabledFieldBackgroundColor"
|
android:background="?attr/voipFormDisabledFieldBackgroundColor"
|
||||||
android:hint="@string/conference_schedule_description_hint"
|
android:hint="@string/conference_schedule_description_hint"
|
||||||
|
|
|
@ -105,7 +105,8 @@
|
||||||
android:background="@drawable/led_background"
|
android:background="@drawable/led_background"
|
||||||
android:padding="@dimen/contact_presence_big_badge_padding"
|
android:padding="@dimen/contact_presence_big_badge_padding"
|
||||||
app:presenceIcon="@{viewModel.presenceStatus}"
|
app:presenceIcon="@{viewModel.presenceStatus}"
|
||||||
android:visibility="@{viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" />
|
android:visibility="@{viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<data>
|
<data>
|
||||||
<import type="android.view.View"/>
|
<import type="android.view.View"/>
|
||||||
|
@ -54,7 +55,8 @@
|
||||||
android:background="@drawable/led_background"
|
android:background="@drawable/led_background"
|
||||||
android:padding="@dimen/contact_presence_badge_padding"
|
android:padding="@dimen/contact_presence_badge_padding"
|
||||||
app:presenceIcon="@{viewModel.presenceStatus}"
|
app:presenceIcon="@{viewModel.presenceStatus}"
|
||||||
android:visibility="@{viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}" />
|
android:visibility="@{viewModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE, default=gone}"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/right"
|
android:id="@+id/right"
|
||||||
|
@ -72,7 +74,7 @@
|
||||||
android:src="@drawable/linphone_logo_tinted"
|
android:src="@drawable/linphone_logo_tinted"
|
||||||
android:layout_marginRight="10dp"
|
android:layout_marginRight="10dp"
|
||||||
android:contentDescription="@string/content_description_linphone_user"
|
android:contentDescription="@string/content_description_linphone_user"
|
||||||
android:visibility="@{viewModel.hasPresence() ? View.VISIBLE : View.GONE}" />
|
android:visibility="@{viewModel.hasLongTermPresence ? View.VISIBLE : View.GONE, default=gone}" />
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:onClick="@{() -> selectionListViewModel.onToggleSelect(position)}"
|
android:onClick="@{() -> selectionListViewModel.onToggleSelect(position)}"
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue