diff --git a/.gitignore b/.gitignore
index 56cbd2768..6c785d36f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,5 +24,6 @@ linphone-sdk-android/*.aar
app/debug
app/release
app/releaseAppBundle
+app/releaseWithCrashlytics
keystore.properties
app/src/main/res/xml/contacts.xml
diff --git a/.gitlab-ci-files/job-android.yml b/.gitlab-ci-files/job-android.yml
index 1127eff87..98fae62c7 100644
--- a/.gitlab-ci-files/job-android.yml
+++ b/.gitlab-ci-files/job-android.yml
@@ -7,6 +7,7 @@ job-android:
before_script:
- if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then eval $(ssh-agent -s); fi
- if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then echo "$SCP_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null; fi
+ - echo "$ANDROID_SETTINGS_GRADLE" > settings.gradle
script:
- sdkmanager
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8d361ca4c..f543da7f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,33 +10,58 @@ Group changes to describe their impact on the project, as follows:
Fixed for any bug fixes.
Security to invite users to upgrade in case of vulnerabilities.
-## [4.6.0] - Unreleased
+## [4.7.0] - Unreleased
+
+## [4.6.0] - 2022-02-09
### Added
- Reply to chat message feature (with original message preview)
+- Swipe action on chat messages to reply / delete
- Voice recordings in chat feature
- Allow video recording in chat file sharing
- Unread messages indicator in chat conversation that separates read & unread messages
-- Notify incoming/outgoing calls on bluetooth devices using self-managed connections from telecom manager API
+- Notify incoming/outgoing calls on bluetooth devices using self-managed connections from telecom manager API (disables SDK audio focus)
+- Ask Android to not process what user types in an encrypted chat room to improve privacy, see [IME_FLAG_NO_PERSONALIZED_LEARNING](https://developer.android.com/reference/android/view/inputmethod/EditorInfo#IME_FLAG_NO_PERSONALIZED_LEARNING)
+- SIP URIs in chat messages are clickable to easily initiate a call
- New video call UI on foldable device like Galaxy Z Fold
- Setting to automatically record all calls
+- When using a physical keyboard, use left control + enter keys to send message
+- Using CallStyle notifications for calls for devices running Android 12 or newer
+- New fragment explaining generic SIP account limitations contrary to sip.linphone.org SIP accounts
+- Link to Weblate added in about page
### Changed
- UI has been reworked around SlidingPane component to better handle tablets & foldable devices
- No longer scroll to bottom of chat room when new messages are received, a new button shows up to do it and it displays conversation's unread messages count
- Animations have been replaced to use com.google.android.material.transition ones
- Using new [Unified Content API](https://developer.android.com/about/versions/12/features/unified-content-api) to share files from keyboard (or other sources)
+- Received messages are now trimmed
- Bumped dependencies, gradle updated from 4.2.2 to 7.0.2
- Target Android SDK version set to 31 (Android 12)
+- Splashscreen is using new APIs
- SDK updated to 5.1.0 release
+- Updated translations
### Fixed
- Chat notifications disappearing when app restarts
- "Infinite backstack", now each view is stored (at most) once in the backstack
+- Voice messages / call recordings will be played on headset/headphones instead of speaker, if possible
- Going back to the dialer when pressing back in a chat room after clicking on a chat message notification
+- Missing international prefix / phone number in assistant after granting permission
+- Display issue for incoming call notification preventing to use answer/hangup actions on some Xiaomi devices (like Redmi Note 9S)
+- Missing foreground service notification for background mode
### Removed
+- Launcher Activity has been replaced by [Splash Screen API](https://developer.android.com/reference/kotlin/androidx/core/splashscreen/SplashScreen)
+- Dialer will no longer make DTMF sound when pressing digits
+- Launcher activity
- Global push notification setting in Network, use the switch in each Account instead
+- No longer need to monitor device rotation and give information to the Core, it does it by itself
+
+## [4.5.6] - 2021-11-08
+
+### Changed
+- SDK updated to 5.0.49
## [4.5.5] - 2021-10-28
@@ -79,16 +104,11 @@ Group changes to describe their impact on the project, as follows:
- Fixed various crashes & other issues
- SDK bumped to 5.0.10
-## [4.5.1] - Unreleased
-
-### Added
-- Reply to chat message feature
-- Voice recordings messages
+## [4.5.1] - 2021-07-15
### Changed
-- Navigation was reworked using SlidingPane widget, reducing code & improving UI on foldables
-
-### Removed
+- Bugs & crashes have been fixed
+- SDK bumped to 5.0.1
## [4.5.0] - 2021-07-08
diff --git a/README.md b/README.md
index f04ebdbaa..4715b240a 100644
--- a/README.md
+++ b/README.md
@@ -97,7 +97,9 @@ Also check you have built the SDK for the right CPU architecture using the `-DLI
- Push notification might not work when app has been started by Android Studio consecutively to an install. Remove the app from the recent activity view and start it again using the launcher icon to resolve this.
-## Troubleshouting
+## Troubleshooting
+
+### Behavior issue
When submitting an issue on our [Github repository](https://github.com/BelledonneCommunications/linphone-android), please follow the template and attach the matching library logs:
@@ -105,7 +107,32 @@ When submitting an issue on our [Github repository](https://github.com/Belledonn
2. Then restart the app, reproduce the issue and upload the logs using the `Send logs` button on the About page.
-3. Finally paste the link to the uploaded logs (link is already in the clipboard after a sucessful upload).
+3. Finally paste the link to the uploaded logs (link is already in the clipboard after a successful upload).
+
+### Native crash
+
+First of all, to be able to get a symbolized stack trace, you need the debug version of our libraries.
+
+If you haven't built the SDK locally (see [building a local SDK](#BuildingalocalSDK)), here's how to get them:
+
+1. Go to our [maven repository](https://download.linphone.org/maven_repository/org/linphone/linphone-sdk-android-debug/), in the linphone-android-debug directory.
+
+2. Download the AAR file with **the exact same version** as the AAR that was used to generate the crash's stacktrace.
+
+3. Extract the AAR somewhere on your computer (it's a simple ZIP file even it's doesn't have the extension). Libraries are stored inside the ```jni``` folder (a directory for each architectured built, usually ```arm64-v8a, armeabi-v7a, x86_64 and x86```).
+
+4. To get consistent with locally built SDK, rename the ```jni``` directory into ```libs-debug```.
+
+Now you need the ```ndk-stack``` tool and possibly ```adb logcat```.
+
+If your computer isn't used for Android development, you can download those tools from [Google website](https://developer.android.com/studio#downloads), in the ```Command line tools only``` section.
+
+Once you have the debug libraries and the proper tools installed, you can use the ```ndk-stack``` tool to symbolize your stacktrace. Note that you also need to know the architecture (armv7, arm64, x86, etc...) of the libraries that were used.
+
+Here's how to get the stacktrace and the right architecture from a device plugged to your computer:
+```
+adb logcat -d | ndk-stack -sym ./libs-debug/`adb shell getprop ro.product.cpu.abi | tr -d '\r'`
+```
## Create an APK with a different package name
@@ -137,6 +164,6 @@ Due to the full app rewrite we can't re-use previous translations, so we'll be v
In order to submit a patch for inclusion in linphone's source code:
1. First make sure your patch applies to latest git sources before submitting: patches made to old versions can't and won't be merged.
-2. Fill out and send us an email with the link of pullrequest and the [Contributor Agreement](https://linphone.org/sites/default/files/bc-contributor-agreement_0.pdf) for your patch to be included in the git tree.
+2. Fill out and send us an email with the link of pull-request and the [Contributor Agreement](https://linphone.org/sites/default/files/bc-contributor-agreement_0.pdf) for your patch to be included in the git tree.
The goal of this agreement to grant us peaceful exercise of our rights on the linphone source code, while not losing your rights on your contribution.
diff --git a/app/build.gradle b/app/build.gradle
index 34c884a21..e4e215d34 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -5,6 +5,9 @@ plugins {
id 'org.jlleitschuh.gradle.ktlint'
}
+def appVersionName = "4.6.0"
+def appVersionCode = 40600 // 4.06.00
+
static def getPackageName() {
return "org.linphone"
}
@@ -24,7 +27,7 @@ if (crashlyticsEnabled) {
def gitBranch = new ByteArrayOutputStream()
task getGitVersion() {
- def gitVersion = "4.7.0"
+ def gitVersion = appVersionName
def gitVersionStream = new ByteArrayOutputStream()
def gitCommitsCount = new ByteArrayOutputStream()
def gitCommitHash = new ByteArrayOutputStream()
@@ -52,9 +55,9 @@ task getGitVersion() {
} else {
gitVersion = gitVersionStream.toString().trim() + "." + gitCommitsCount.toString().trim() + "+" + gitCommitHash.toString().trim()
}
- println("Git version: " + gitVersion)
+ println("Git version: " + gitVersion + " (" + appVersionCode + ")")
} catch (ignored) {
- println("Git not found")
+ println("Git not found, using " + gitVersion + " (" + appVersionCode + ")")
}
project.version = gitVersion
}
@@ -84,7 +87,7 @@ android {
defaultConfig {
minSdkVersion 23
targetSdkVersion 31
- versionCode 4700
+ versionCode appVersionCode
versionName "${project.version}"
applicationId getPackageName()
}
@@ -95,7 +98,7 @@ android {
}
// 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 == "releaseAppBundle") {
+ if (variant.buildType.name == "release" || variant.buildType.name == "releaseWithCrashlytics") {
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
linphone_file_provider: getPackageName() + ".fileprovider",
appLabel: "@string/app_name",
@@ -143,8 +146,8 @@ android {
initWith release
resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString()
- if (crashlyticsEnabled) {
+ if (crashlyticsEnabled) {
firebaseCrashlytics {
nativeSymbolUploadEnabled true
unstrippedNativeLibsDir file(LinphoneSdkBuildDir + '/libs-debug/').toString()
@@ -161,14 +164,13 @@ android {
resValue "string", "sync_account_type", getPackageName() + ".sync"
resValue "string", "file_provider", getPackageName() + ".debug.fileprovider"
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"
+ resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString()
if (!firebaseEnabled) {
resValue "string", "gcm_defaultSenderId", "none"
}
- resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString()
if (crashlyticsEnabled) {
-
firebaseCrashlytics {
nativeSymbolUploadEnabled true
unstrippedNativeLibsDir file(LinphoneSdkBuildDir + '/libs-debug/').toString()
@@ -191,47 +193,28 @@ android {
}
}
-repositories {
- maven {
- name "local linphone-sdk maven repository"
- url file(LinphoneSdkBuildDir + '/maven_repository/')
- content {
- includeGroup "org.linphone"
- }
- }
-
- maven {
- name "linphone.org maven repository"
- url "https://download.linphone.org/maven_repository"
- content {
- includeGroup "org.linphone"
- }
- }
-}
-
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
- implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
-
- implementation 'androidx.appcompat:appcompat:1.3.1'
+ implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.media:media:1.4.3'
- implementation 'androidx.fragment:fragment-ktx:1.4.0-beta01'
+ implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation 'androidx.core:core-ktx:1.7.0'
- def nav_version = "2.4.0-beta01"
+ def nav_version = "2.4.0"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
- implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0-beta01"
- implementation "androidx.window:window:1.0.0-beta03"
+ implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
+ implementation "androidx.window:window:1.0.0"
- implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "androidx.security:security-crypto-ktx:1.1.0-alpha03"
+ implementation 'androidx.core:core-splashscreen:1.0.0-beta01'
- implementation 'com.google.android.material:material:1.4.0'
+ implementation 'com.google.android.material:material:1.5.0'
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'androidx.emoji:emoji:1.1.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7fb8213c6..88547b8ef 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -42,25 +42,21 @@
android:theme="@style/AppTheme"
android:allowNativeHeapPointerTagging="false">
-
+ android:launchMode="singleTask"
+ android:windowSoftInputMode="adjustResize"
+ android:theme="@style/AppSplashScreenTheme">
+
+
+
-
-
-
-
diff --git a/app/src/main/assets/assistant_default_values b/app/src/main/assets/assistant_default_values
index 7e5d7543b..86d8e0be7 100644
--- a/app/src/main/assets/assistant_default_values
+++ b/app/src/main/assets/assistant_default_values
@@ -16,6 +16,7 @@
0
+ 0
diff --git a/app/src/main/assets/assistant_linphone_default_values b/app/src/main/assets/assistant_linphone_default_values
index f7561ee9b..9f2badee0 100644
--- a/app/src/main/assets/assistant_linphone_default_values
+++ b/app/src/main/assets/assistant_linphone_default_values
@@ -16,6 +16,7 @@
sip.linphone.org
sip:conference-factory@sip.linphone.org
1
+ 1
stun.linphone.org
diff --git a/app/src/main/java/org/linphone/activities/GenericActivity.kt b/app/src/main/java/org/linphone/activities/GenericActivity.kt
index 0eec20c2b..f6c8a596e 100644
--- a/app/src/main/java/org/linphone/activities/GenericActivity.kt
+++ b/app/src/main/java/org/linphone/activities/GenericActivity.kt
@@ -25,13 +25,12 @@ import android.content.res.Configuration
import android.os.Bundle
import android.util.DisplayMetrics
import android.view.Display
-import android.view.Surface
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.lifecycleScope
import androidx.navigation.ActivityNavigator
import androidx.window.layout.FoldingFeature
-import androidx.window.layout.WindowInfoRepository.Companion.windowInfoRepository
+import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
import java.util.*
import kotlinx.coroutines.Dispatchers
@@ -58,9 +57,12 @@ abstract class GenericActivity : AppCompatActivity() {
ensureCoreExists(applicationContext)
lifecycleScope.launch(Dispatchers.Main) {
- windowInfoRepository().windowLayoutInfo.collect { newLayoutInfo ->
- updateCurrentLayout(newLayoutInfo)
- }
+ WindowInfoTracker
+ .getOrCreate(this@GenericActivity)
+ .windowLayoutInfo(this@GenericActivity)
+ .collect { newLayoutInfo ->
+ updateCurrentLayout(newLayoutInfo)
+ }
}
requestedOrientation = if (corePreferences.forcePortrait) {
@@ -97,18 +99,6 @@ abstract class GenericActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
- var degrees = 270
- val orientation = windowManager.defaultDisplay.rotation
- when (orientation) {
- Surface.ROTATION_0 -> degrees = 0
- Surface.ROTATION_90 -> degrees = 270
- Surface.ROTATION_180 -> degrees = 180
- Surface.ROTATION_270 -> degrees = 90
- }
- Log.i("[Generic Activity] Device orientation is $degrees (raw value is $orientation)")
- val rotation = (360 - degrees) % 360
- coreContext.core.deviceRotation = rotation
-
// Remove service notification if it has been started by device boot
coreContext.notificationsManager.stopForegroundNotificationIfPossible()
}
diff --git a/app/src/main/java/org/linphone/activities/GenericFragment.kt b/app/src/main/java/org/linphone/activities/GenericFragment.kt
index 94bbfaffa..8cbd2a1d0 100644
--- a/app/src/main/java/org/linphone/activities/GenericFragment.kt
+++ b/app/src/main/java/org/linphone/activities/GenericFragment.kt
@@ -28,8 +28,12 @@ import androidx.core.view.doOnPreDraw
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
+import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.google.android.material.transition.MaterialSharedAxis
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.core.tools.Log
@@ -38,9 +42,17 @@ abstract class GenericFragment : Fragment() {
protected val binding get() = _binding!!
protected var useMaterialSharedAxisXForwardAnimation = true
+ protected fun isBindingAvailable(): Boolean {
+ return _binding != null
+ }
+
protected val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
- goBack()
+ lifecycleScope.launch {
+ withContext(Dispatchers.Main) {
+ goBack()
+ }
+ }
}
}
diff --git a/app/src/main/java/org/linphone/activities/Navigation.kt b/app/src/main/java/org/linphone/activities/Navigation.kt
index e8f289c27..5d6bd190a 100644
--- a/app/src/main/java/org/linphone/activities/Navigation.kt
+++ b/app/src/main/java/org/linphone/activities/Navigation.kt
@@ -72,6 +72,30 @@ internal fun MainActivity.navigateToDialer(args: Bundle?) {
)
}
+internal fun MainActivity.navigateToChatRooms(args: Bundle? = null) {
+ findNavController(R.id.nav_host_fragment).navigate(
+ R.id.action_global_masterChatRoomsFragment,
+ args,
+ popupTo(R.id.masterChatRoomsFragment, true)
+ )
+}
+
+internal fun MainActivity.navigateToChatRoom(localAddress: String?, peerAddress: String?) {
+ val deepLink = "linphone-android://chat-room/$localAddress/$peerAddress"
+ findNavController(R.id.nav_host_fragment).navigate(
+ Uri.parse(deepLink),
+ popupTo(R.id.masterChatRoomsFragment, true)
+ )
+}
+
+internal fun MainActivity.navigateToContact(contactId: String?) {
+ val deepLink = "linphone-android://contact/view/$contactId"
+ findNavController(R.id.nav_host_fragment).navigate(
+ Uri.parse(deepLink),
+ popupTo(R.id.masterContactsFragment, true)
+ )
+}
+
/* Tabs fragment related */
internal fun TabsFragment.navigateToCallHistory() {
@@ -79,9 +103,8 @@ internal fun TabsFragment.navigateToCallHistory() {
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_masterCallLogsFragment
R.id.dialerFragment -> R.id.action_dialerFragment_to_masterCallLogsFragment
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_masterCallLogsFragment
- else -> 0
+ else -> R.id.action_global_masterCallLogsFragment
}
- if (action == 0) return
findNavController().navigate(
action,
null,
@@ -94,9 +117,8 @@ internal fun TabsFragment.navigateToContacts() {
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_masterContactsFragment
R.id.dialerFragment -> R.id.action_dialerFragment_to_masterContactsFragment
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_masterContactsFragment
- else -> 0
+ else -> R.id.action_global_masterContactsFragment
}
- if (action == 0) return
findNavController().navigate(
action,
null,
@@ -109,9 +131,8 @@ internal fun TabsFragment.navigateToDialer() {
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_dialerFragment
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_dialerFragment
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_dialerFragment
- else -> 0
+ else -> R.id.action_global_dialerFragment
}
- if (action == 0) return
findNavController().navigate(
action,
null,
@@ -124,9 +145,8 @@ internal fun TabsFragment.navigateToChatRooms() {
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_masterChatRoomsFragment
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_masterChatRoomsFragment
R.id.dialerFragment -> R.id.action_dialerFragment_to_masterChatRoomsFragment
- else -> 0
+ else -> R.id.action_global_masterChatRoomsFragment
}
- if (action == 0) return
findNavController().navigate(
action,
null,
@@ -298,7 +318,15 @@ internal fun DetailChatRoomFragment.navigateToEmptyChatRoom() {
findNavController().navigate(
R.id.action_global_emptyChatFragment,
null,
- popupTo(R.id.emptyChatFragment, true)
+ popupTo(R.id.detailChatRoomFragment, true)
+ )
+}
+
+internal fun DetailChatRoomFragment.navigateToDialer(args: Bundle?) {
+ findMasterNavController().navigate(
+ R.id.action_global_dialerFragment,
+ args,
+ popupTo(R.id.dialerFragment, true)
)
}
@@ -317,7 +345,7 @@ internal fun ChatRoomCreationFragment.navigateToChatRoom(args: Bundle) {
findNavController().navigate(
R.id.action_chatRoomCreationFragment_to_detailChatRoomFragment,
args,
- popupTo(R.id.detailChatRoomFragment, true)
+ popupTo(R.id.chatRoomCreationFragment, true)
)
}
}
@@ -326,7 +354,7 @@ internal fun ChatRoomCreationFragment.navigateToEmptyChatRoom() {
findNavController().navigate(
R.id.action_global_emptyChatFragment,
null,
- popupTo(R.id.emptyChatFragment, true)
+ popupTo(R.id.chatRoomCreationFragment, true)
)
}
@@ -782,10 +810,10 @@ internal fun WelcomeFragment.navigateToAccountLogin() {
}
}
-internal fun WelcomeFragment.navigateToGenericLogin() {
+internal fun WelcomeFragment.navigateToGenericLoginWarning() {
if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
findNavController().navigate(
- R.id.action_welcomeFragment_to_genericAccountLoginFragment,
+ R.id.action_welcomeFragment_to_genericAccountWarningFragment,
null,
popupTo()
)
@@ -822,6 +850,16 @@ internal fun AccountLoginFragment.navigateToPhoneAccountValidation(args: Bundle?
}
}
+internal fun GenericAccountWarningFragment.navigateToGenericLogin() {
+ if (findNavController().currentDestination?.id == R.id.genericAccountWarningFragment) {
+ findNavController().navigate(
+ R.id.action_genericAccountWarningFragment_to_genericAccountLoginFragment,
+ null,
+ popupTo(R.id.welcomeFragment, popUpInclusive = false)
+ )
+ }
+}
+
internal fun GenericAccountLoginFragment.navigateToEchoCancellerCalibration() {
if (findNavController().currentDestination?.id == R.id.genericAccountLoginFragment) {
findNavController().navigate(
diff --git a/app/src/main/java/org/linphone/activities/SnackBarActivity.kt b/app/src/main/java/org/linphone/activities/SnackBarActivity.kt
index 7eb57e548..b28d8e5d4 100644
--- a/app/src/main/java/org/linphone/activities/SnackBarActivity.kt
+++ b/app/src/main/java/org/linphone/activities/SnackBarActivity.kt
@@ -21,5 +21,6 @@ package org.linphone.activities
interface SnackBarActivity {
fun showSnackBar(resourceId: Int)
+ fun showSnackBar(resourceId: Int, action: Int, listener: () -> Unit)
fun showSnackBar(message: String)
}
diff --git a/app/src/main/java/org/linphone/activities/assistant/AssistantActivity.kt b/app/src/main/java/org/linphone/activities/assistant/AssistantActivity.kt
index f3524463d..52fd813f9 100644
--- a/app/src/main/java/org/linphone/activities/assistant/AssistantActivity.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/AssistantActivity.kt
@@ -23,6 +23,7 @@ import android.os.Bundle
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.snackbar.Snackbar
+import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.activities.GenericActivity
import org.linphone.activities.SnackBarActivity
@@ -40,12 +41,23 @@ class AssistantActivity : GenericActivity(), SnackBarActivity {
sharedViewModel = ViewModelProvider(this)[SharedAssistantViewModel::class.java]
coordinator = findViewById(R.id.coordinator)
+
+ corePreferences.firstStart = false
}
override fun showSnackBar(resourceId: Int) {
Snackbar.make(coordinator, resourceId, Snackbar.LENGTH_LONG).show()
}
+ override fun showSnackBar(resourceId: Int, action: Int, listener: () -> Unit) {
+ Snackbar
+ .make(findViewById(R.id.coordinator), resourceId, Snackbar.LENGTH_LONG)
+ .setAction(action) {
+ listener()
+ }
+ .show()
+ }
+
override fun showSnackBar(message: String) {
Snackbar.make(coordinator, message, Snackbar.LENGTH_LONG).show()
}
diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/AbstractPhoneFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/AbstractPhoneFragment.kt
index 7b92f5652..fbc2db617 100644
--- a/app/src/main/java/org/linphone/activities/assistant/fragments/AbstractPhoneFragment.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/AbstractPhoneFragment.kt
@@ -20,6 +20,7 @@
package org.linphone.activities.assistant.fragments
+import android.annotation.TargetApi
import android.content.pm.PackageManager
import androidx.databinding.ViewDataBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -28,10 +29,15 @@ import org.linphone.activities.GenericFragment
import org.linphone.activities.assistant.viewmodels.AbstractPhoneViewModel
import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log
+import org.linphone.mediastream.Version
import org.linphone.utils.PermissionHelper
import org.linphone.utils.PhoneNumberUtils
abstract class AbstractPhoneFragment : GenericFragment() {
+ companion object {
+ const val READ_PHONE_STATE_PERMISSION_REQUEST_CODE = 0
+ }
+
abstract val viewModel: AbstractPhoneViewModel
override fun onRequestPermissionsResult(
@@ -39,7 +45,7 @@ abstract class AbstractPhoneFragment : GenericFragment()
permissions: Array,
grantResults: IntArray
) {
- if (requestCode == 0) {
+ if (requestCode == READ_PHONE_STATE_PERMISSION_REQUEST_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.i("[Assistant] READ_PHONE_STATE/READ_PHONE_NUMBERS permission granted")
updateFromDeviceInfo()
@@ -49,11 +55,12 @@ abstract class AbstractPhoneFragment : GenericFragment()
}
}
- protected fun checkPermission() {
+ @TargetApi(Version.API23_MARSHMALLOW_60)
+ protected fun checkPermissions() {
if (!resources.getBoolean(R.bool.isTablet)) {
if (!PermissionHelper.get().hasReadPhoneStateOrPhoneNumbersPermission()) {
Log.i("[Assistant] Asking for READ_PHONE_STATE/READ_PHONE_NUMBERS permission")
- Compatibility.requestReadPhoneStateOrNumbersPermission(requireActivity(), 0)
+ Compatibility.requestReadPhoneStateOrNumbersPermission(this, READ_PHONE_STATE_PERMISSION_REQUEST_CODE)
} else {
updateFromDeviceInfo()
}
diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/AccountLoginFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/AccountLoginFragment.kt
index 1a6e1f264..4cf945451 100644
--- a/app/src/main/java/org/linphone/activities/assistant/fragments/AccountLoginFragment.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/AccountLoginFragment.kt
@@ -35,6 +35,7 @@ import org.linphone.activities.main.viewmodels.DialogViewModel
import org.linphone.activities.navigateToEchoCancellerCalibration
import org.linphone.activities.navigateToPhoneAccountValidation
import org.linphone.databinding.AssistantAccountLoginFragmentBinding
+import org.linphone.mediastream.Version
import org.linphone.utils.DialogUtils
class AccountLoginFragment : AbstractPhoneFragment() {
@@ -52,7 +53,10 @@ class AccountLoginFragment : AbstractPhoneFragment
- (requireActivity() as AssistantActivity).showSnackBar(message)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { message ->
+ (requireActivity() as AssistantActivity).showSnackBar(message)
}
- )
+ }
- checkPermission()
+ if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
+ checkPermissions()
+ }
}
}
diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/EchoCancellerCalibrationFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/EchoCancellerCalibrationFragment.kt
index 087f7fea0..626e35017 100644
--- a/app/src/main/java/org/linphone/activities/assistant/fragments/EchoCancellerCalibrationFragment.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/EchoCancellerCalibrationFragment.kt
@@ -31,6 +31,10 @@ import org.linphone.databinding.AssistantEchoCancellerCalibrationFragmentBinding
import org.linphone.utils.PermissionHelper
class EchoCancellerCalibrationFragment : GenericFragment() {
+ companion object {
+ const val RECORD_AUDIO_PERMISSION_REQUEST_CODE = 0
+ }
+
private lateinit var viewModel: EchoCancellerCalibrationViewModel
override fun getLayoutId(): Int = R.layout.assistant_echo_canceller_calibration_fragment
@@ -44,17 +48,16 @@ class EchoCancellerCalibrationFragment : GenericFragment,
grantResults: IntArray
) {
- val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
- if (granted) {
- Log.i("[Echo Canceller Calibration] RECORD_AUDIO permission granted")
- viewModel.startEchoCancellerCalibration()
- } else {
- Log.w("[Echo Canceller Calibration] RECORD_AUDIO permission denied")
- requireActivity().finish()
+ if (requestCode == RECORD_AUDIO_PERMISSION_REQUEST_CODE) {
+ val granted =
+ grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
+ if (granted) {
+ Log.i("[Echo Canceller Calibration] RECORD_AUDIO permission granted")
+ viewModel.startEchoCancellerCalibration()
+ } else {
+ Log.w("[Echo Canceller Calibration] RECORD_AUDIO permission denied")
+ requireActivity().finish()
+ }
}
}
}
diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountCreationFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountCreationFragment.kt
index fa5c9d718..d9ed6aa89 100644
--- a/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountCreationFragment.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountCreationFragment.kt
@@ -50,21 +50,19 @@ class EmailAccountCreationFragment : GenericFragment
- (requireActivity() as AssistantActivity).showSnackBar(message)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { message ->
+ (requireActivity() as AssistantActivity).showSnackBar(message)
}
- )
+ }
}
}
diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountValidationFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountValidationFragment.kt
index dd2f5aeef..e6f4cd409 100644
--- a/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountValidationFragment.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountValidationFragment.kt
@@ -49,27 +49,25 @@ class EmailAccountValidationFragment : GenericFragment
- (requireActivity() as AssistantActivity).showSnackBar(message)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { message ->
+ (requireActivity() as AssistantActivity).showSnackBar(message)
}
- )
+ }
}
}
diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountLoginFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountLoginFragment.kt
index 2b81c1336..9dc119313 100644
--- a/app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountLoginFragment.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountLoginFragment.kt
@@ -54,52 +54,50 @@ class GenericAccountLoginFragment : GenericFragment
- (requireActivity() as AssistantActivity).showSnackBar(message)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { message ->
+ (requireActivity() as AssistantActivity).showSnackBar(message)
}
- )
+ }
}
}
diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountWarningFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountWarningFragment.kt
new file mode 100644
index 000000000..8c41c0653
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountWarningFragment.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2010-2022 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.activities.assistant.fragments
+
+import android.os.Bundle
+import android.view.View
+import org.linphone.R
+import org.linphone.activities.GenericFragment
+import org.linphone.activities.navigateToGenericLogin
+import org.linphone.databinding.AssistantGenericAccountWarningFragmentBinding
+
+class GenericAccountWarningFragment : GenericFragment() {
+ override fun getLayoutId(): Int = R.layout.assistant_generic_account_warning_fragment
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ binding.lifecycleOwner = viewLifecycleOwner
+
+ binding.setUnderstoodClickListener {
+ navigateToGenericLogin()
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountCreationFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountCreationFragment.kt
index f396bd059..cce9f7a9c 100644
--- a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountCreationFragment.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountCreationFragment.kt
@@ -29,8 +29,10 @@ import org.linphone.activities.assistant.viewmodels.PhoneAccountCreationViewMode
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
import org.linphone.activities.navigateToPhoneAccountValidation
import org.linphone.databinding.AssistantPhoneAccountCreationFragmentBinding
+import org.linphone.mediastream.Version
-class PhoneAccountCreationFragment : AbstractPhoneFragment() {
+class PhoneAccountCreationFragment :
+ AbstractPhoneFragment() {
private lateinit var sharedViewModel: SharedAssistantViewModel
override lateinit var viewModel: PhoneAccountCreationViewModel
@@ -45,7 +47,10 @@ class PhoneAccountCreationFragment : AbstractPhoneFragment
- (requireActivity() as AssistantActivity).showSnackBar(message)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { message ->
+ (requireActivity() as AssistantActivity).showSnackBar(message)
}
- )
+ }
- checkPermission()
+ if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
+ checkPermissions()
+ }
}
}
diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt
index 786a540fc..1f7fa1f26 100644
--- a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt
@@ -30,6 +30,7 @@ import org.linphone.activities.navigateToEchoCancellerCalibration
import org.linphone.activities.navigateToPhoneAccountValidation
import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantPhoneAccountLinkingFragmentBinding
+import org.linphone.mediastream.Version
class PhoneAccountLinkingFragment : AbstractPhoneFragment() {
private lateinit var sharedViewModel: SharedAssistantViewModel
@@ -72,39 +73,38 @@ class PhoneAccountLinkingFragment : AbstractPhoneFragment
- (requireActivity() as AssistantActivity).showSnackBar(message)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { message ->
+ (requireActivity() as AssistantActivity).showSnackBar(message)
}
- )
+ }
- checkPermission()
+ if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
+ checkPermissions()
+ }
}
}
diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountValidationFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountValidationFragment.kt
index 534f38234..4bac03647 100644
--- a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountValidationFragment.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountValidationFragment.kt
@@ -59,37 +59,38 @@ class PhoneAccountValidationFragment : GenericFragment {
- coreContext.contactsManager.updateLocalContacts()
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ when {
+ viewModel.isLogin.value == true || viewModel.isCreation.value == true -> {
+ coreContext.contactsManager.updateLocalContacts()
- if (coreContext.core.isEchoCancellerCalibrationRequired) {
- navigateToEchoCancellerCalibration()
- } else {
- requireActivity().finish()
- }
- }
- viewModel.isLinking.value == true -> {
- val args = Bundle()
- args.putString("Identity", "sip:${viewModel.accountCreator.username}@${viewModel.accountCreator.domain}")
- navigateToAccountSettings(args)
+ if (coreContext.core.isEchoCancellerCalibrationRequired) {
+ navigateToEchoCancellerCalibration()
+ } else {
+ requireActivity().finish()
}
}
+ viewModel.isLinking.value == true -> {
+ val args = Bundle()
+ args.putString(
+ "Identity",
+ "sip:${viewModel.accountCreator.username}@${viewModel.accountCreator.domain}"
+ )
+ navigateToAccountSettings(args)
+ }
}
}
- )
+ }
viewModel.onErrorEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { message ->
- (requireActivity() as AssistantActivity).showSnackBar(message)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { message ->
+ (requireActivity() as AssistantActivity).showSnackBar(message)
}
- )
+ }
val clipboard = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
clipboard.addPrimaryClipChangedListener {
diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/QrCodeFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/QrCodeFragment.kt
index 2e110863e..45daa8761 100644
--- a/app/src/main/java/org/linphone/activities/assistant/fragments/QrCodeFragment.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/QrCodeFragment.kt
@@ -34,6 +34,10 @@ import org.linphone.databinding.AssistantQrCodeFragmentBinding
import org.linphone.utils.PermissionHelper
class QrCodeFragment : GenericFragment() {
+ companion object {
+ const val CAMERA_PERMISSION_REQUEST_CODE = 0
+ }
+
private lateinit var sharedViewModel: SharedAssistantViewModel
private lateinit var viewModel: QrCodeViewModel
@@ -52,19 +56,18 @@ class QrCodeFragment : GenericFragment() {
binding.viewModel = viewModel
viewModel.qrCodeFoundEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { url ->
- sharedViewModel.remoteProvisioningUrl.value = url
- findNavController().navigateUp()
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { url ->
+ sharedViewModel.remoteProvisioningUrl.value = url
+ findNavController().navigateUp()
}
- )
+ }
viewModel.setBackCamera()
if (!PermissionHelper.required(requireContext()).hasCameraPermission()) {
Log.i("[QR Code] Asking for CAMERA permission")
- requestPermissions(arrayOf(android.Manifest.permission.CAMERA), 0)
+ requestPermissions(arrayOf(android.Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE)
}
}
@@ -72,14 +75,14 @@ class QrCodeFragment : GenericFragment() {
super.onResume()
coreContext.core.nativePreviewWindowId = binding.qrCodeCaptureTexture
- coreContext.core.enableQrcodeVideoPreview(true)
- coreContext.core.enableVideoPreview(true)
+ coreContext.core.isQrcodeVideoPreviewEnabled = true
+ coreContext.core.isVideoPreviewEnabled = true
}
override fun onPause() {
coreContext.core.nativePreviewWindowId = null
- coreContext.core.enableQrcodeVideoPreview(false)
- coreContext.core.enableVideoPreview(false)
+ coreContext.core.isQrcodeVideoPreviewEnabled = false
+ coreContext.core.isVideoPreviewEnabled = false
super.onPause()
}
@@ -89,14 +92,17 @@ class QrCodeFragment : GenericFragment() {
permissions: Array,
grantResults: IntArray
) {
- val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
- if (granted) {
- Log.i("[QR Code] CAMERA permission granted")
- coreContext.core.reloadVideoDevices()
- viewModel.setBackCamera()
- } else {
- Log.w("[QR Code] CAMERA permission denied")
- findNavController().navigateUp()
+ if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
+ val granted =
+ grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
+ if (granted) {
+ Log.i("[QR Code] CAMERA permission granted")
+ coreContext.core.reloadVideoDevices()
+ viewModel.setBackCamera()
+ } else {
+ Log.w("[QR Code] CAMERA permission denied")
+ findNavController().navigateUp()
+ }
}
}
}
diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/RemoteProvisioningFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/RemoteProvisioningFragment.kt
index 3dad2ddc6..d80738b57 100644
--- a/app/src/main/java/org/linphone/activities/assistant/fragments/RemoteProvisioningFragment.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/RemoteProvisioningFragment.kt
@@ -55,22 +55,21 @@ class RemoteProvisioningFragment : GenericFragment
- if (success) {
- if (coreContext.core.isEchoCancellerCalibrationRequired) {
- navigateToEchoCancellerCalibration()
- } else {
- requireActivity().finish()
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { success ->
+ if (success) {
+ if (coreContext.core.isEchoCancellerCalibrationRequired) {
+ navigateToEchoCancellerCalibration()
} else {
- val activity = requireActivity() as AssistantActivity
- activity.showSnackBar(R.string.assistant_remote_provisioning_failure)
+ requireActivity().finish()
}
+ } else {
+ val activity = requireActivity() as AssistantActivity
+ activity.showSnackBar(R.string.assistant_remote_provisioning_failure)
}
}
- )
+ }
viewModel.urlToFetch.value = sharedViewModel.remoteProvisioningUrl.value ?: coreContext.core.provisioningUri
}
diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/WelcomeFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/WelcomeFragment.kt
index 8afe92371..012f63682 100644
--- a/app/src/main/java/org/linphone/activities/assistant/fragments/WelcomeFragment.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/WelcomeFragment.kt
@@ -35,7 +35,6 @@ import org.linphone.activities.*
import org.linphone.activities.assistant.viewmodels.WelcomeViewModel
import org.linphone.activities.navigateToAccountLogin
import org.linphone.activities.navigateToEmailAccountCreation
-import org.linphone.activities.navigateToGenericLogin
import org.linphone.activities.navigateToRemoteProvisioning
import org.linphone.databinding.AssistantWelcomeFragmentBinding
@@ -65,7 +64,7 @@ class WelcomeFragment : GenericFragment() {
}
binding.setGenericAccountLoginClickListener {
- navigateToGenericLogin()
+ navigateToGenericLoginWarning()
}
binding.setRemoteProvisioningClickListener {
@@ -73,11 +72,10 @@ class WelcomeFragment : GenericFragment() {
}
viewModel.termsAndPrivacyAccepted.observe(
- viewLifecycleOwner,
- {
- if (it) corePreferences.readAndAgreeTermsAndPrivacy = true
- }
- )
+ viewLifecycleOwner
+ ) {
+ if (it) corePreferences.readAndAgreeTermsAndPrivacy = true
+ }
setUpTermsAndPrivacyLinks()
}
diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/AbstractPhoneViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/AbstractPhoneViewModel.kt
index 3daca0267..bfc4c0e10 100644
--- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/AbstractPhoneViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/AbstractPhoneViewModel.kt
@@ -56,14 +56,19 @@ abstract class AbstractPhoneViewModel(val accountCreator: AccountCreator) :
}
fun updateFromPhoneNumberAndOrDialPlan(number: String?, dialPlan: DialPlan?) {
+ val internationalPrefix = "+${dialPlan?.countryCallingCode}"
if (dialPlan != null) {
Log.i("[Assistant] Found prefix from dial plan: ${dialPlan.countryCallingCode}")
- prefix.value = "+${dialPlan.countryCallingCode}"
+ prefix.value = internationalPrefix
}
if (number != null) {
Log.i("[Assistant] Found phone number: $number")
- phoneNumber.value = number!!
+ phoneNumber.value = if (number.startsWith(internationalPrefix)) {
+ number.substring(internationalPrefix.length)
+ } else {
+ number
+ }
}
}
diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/AccountLoginViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/AccountLoginViewModel.kt
index 01d80a054..3067024b3 100644
--- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/AccountLoginViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/AccountLoginViewModel.kt
@@ -25,6 +25,7 @@ import org.linphone.R
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.Event
+import org.linphone.utils.PhoneNumberUtils
class AccountLoginViewModelFactory(private val accountCreator: AccountCreator) :
ViewModelProvider.NewInstanceFactory() {
@@ -220,6 +221,18 @@ class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewM
proxyConfig.isPushNotificationAllowed = true
+ if (proxyConfig.dialPrefix.isNullOrEmpty()) {
+ val dialPlan = PhoneNumberUtils.getDialPlanForCurrentCountry(coreContext.context)
+ if (dialPlan != null) {
+ Log.i("[Assistant] [Account Login] Found dial plan country ${dialPlan.country} with international prefix ${dialPlan.countryCallingCode}")
+ proxyConfig.edit()
+ proxyConfig.dialPrefix = dialPlan.countryCallingCode
+ proxyConfig.done()
+ } else {
+ Log.w("[Assistant] [Account Login] Failed to find dial plan")
+ }
+ }
+
Log.i("[Assistant] [Account Login] Proxy config created")
return true
}
diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountValidationViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountValidationViewModel.kt
index 6032efaeb..1f67ba43d 100644
--- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountValidationViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountValidationViewModel.kt
@@ -22,11 +22,13 @@ package org.linphone.activities.assistant.viewmodels
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
+import org.linphone.LinphoneApplication
import org.linphone.core.AccountCreator
import org.linphone.core.AccountCreatorListenerStub
import org.linphone.core.ProxyConfig
import org.linphone.core.tools.Log
import org.linphone.utils.Event
+import org.linphone.utils.PhoneNumberUtils
class EmailAccountValidationViewModelFactory(private val accountCreator: AccountCreator) :
ViewModelProvider.NewInstanceFactory() {
@@ -106,6 +108,18 @@ class EmailAccountValidationViewModel(val accountCreator: AccountCreator) : View
proxyConfig.isPushNotificationAllowed = true
+ if (proxyConfig.dialPrefix.isNullOrEmpty()) {
+ val dialPlan = PhoneNumberUtils.getDialPlanForCurrentCountry(LinphoneApplication.coreContext.context)
+ if (dialPlan != null) {
+ Log.i("[Assistant] [Account Validation] Found dial plan country ${dialPlan.country} with international prefix ${dialPlan.countryCallingCode}")
+ proxyConfig.edit()
+ proxyConfig.dialPrefix = dialPlan.countryCallingCode
+ proxyConfig.done()
+ } else {
+ Log.w("[Assistant] [Account Validation] Failed to find dial plan")
+ }
+ }
+
Log.i("[Assistant] [Account Validation] Proxy config created")
return true
}
diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/GenericLoginViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/GenericLoginViewModel.kt
index 89fe487f7..6ecdfc9d6 100644
--- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/GenericLoginViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/GenericLoginViewModel.kt
@@ -136,6 +136,7 @@ class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewMo
Log.e("[Assistant] [Generic Login] Account creator couldn't create proxy config")
coreContext.core.removeListener(coreListener)
onErrorEvent.value = Event("Error: Failed to create account object")
+ waitForServerAnswer.value = false
return
}
diff --git a/app/src/main/java/org/linphone/activities/call/CallActivity.kt b/app/src/main/java/org/linphone/activities/call/CallActivity.kt
index 09ea2bb8a..2ef7eb13e 100644
--- a/app/src/main/java/org/linphone/activities/call/CallActivity.kt
+++ b/app/src/main/java/org/linphone/activities/call/CallActivity.kt
@@ -61,40 +61,36 @@ class CallActivity : ProximitySensorActivity() {
sharedViewModel = ViewModelProvider(this)[SharedCallViewModel::class.java]
sharedViewModel.toggleDrawerEvent.observe(
- this,
- {
- it.consume {
- if (binding.statsMenu.isDrawerOpen(Gravity.LEFT)) {
- binding.statsMenu.closeDrawer(binding.sideMenuContent, true)
- } else {
- binding.statsMenu.openDrawer(binding.sideMenuContent, true)
- }
+ this
+ ) {
+ it.consume {
+ if (binding.statsMenu.isDrawerOpen(Gravity.LEFT)) {
+ binding.statsMenu.closeDrawer(binding.sideMenuContent, true)
+ } else {
+ binding.statsMenu.openDrawer(binding.sideMenuContent, true)
}
}
- )
+ }
sharedViewModel.resetHiddenInterfaceTimerInVideoCallEvent.observe(
- this,
- {
- it.consume {
- viewModel.showMomentarily()
- }
+ this
+ ) {
+ it.consume {
+ viewModel.showMomentarily()
}
- )
+ }
viewModel.proximitySensorEnabled.observe(
- this,
- {
- enableProximitySensor(it)
- }
- )
+ this
+ ) {
+ enableProximitySensor(it)
+ }
viewModel.videoEnabled.observe(
- this,
- {
- updateConstraintSetDependingOnFoldingState()
- }
- )
+ this
+ ) {
+ updateConstraintSetDependingOnFoldingState()
+ }
}
override fun onLayoutChanges(foldingFeature: FoldingFeature?) {
diff --git a/app/src/main/java/org/linphone/activities/call/IncomingCallActivity.kt b/app/src/main/java/org/linphone/activities/call/IncomingCallActivity.kt
index 76fb75e3d..c150de7a0 100644
--- a/app/src/main/java/org/linphone/activities/call/IncomingCallActivity.kt
+++ b/app/src/main/java/org/linphone/activities/call/IncomingCallActivity.kt
@@ -79,24 +79,22 @@ class IncomingCallActivity : GenericActivity() {
binding.viewModel = viewModel
viewModel.callEndedEvent.observe(
- this,
- {
- it.consume {
- Log.i("[Incoming Call Activity] Call ended, finish activity")
- finish()
- }
+ this
+ ) {
+ it.consume {
+ Log.i("[Incoming Call Activity] Call ended, finish activity")
+ finish()
}
- )
+ }
viewModel.earlyMediaVideoEnabled.observe(
- this,
- {
- if (it) {
- Log.i("[Incoming Call Activity] Early media video being received, set native window id")
- coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface
- }
+ this
+ ) {
+ if (it) {
+ Log.i("[Incoming Call Activity] Early media video being received, set native window id")
+ coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface
}
- )
+ }
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
val keyguardLocked = keyguardManager.isKeyguardLocked
@@ -139,7 +137,7 @@ class IncomingCallActivity : GenericActivity() {
permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO)
}
- if (viewModel.call.currentParams.videoEnabled() && !PermissionHelper.get().hasCameraPermission()) {
+ if (viewModel.call.currentParams.isVideoEnabled && !PermissionHelper.get().hasCameraPermission()) {
Log.i("[Incoming Call Activity] Asking for CAMERA permission")
permissionsRequiredList.add(Manifest.permission.CAMERA)
}
diff --git a/app/src/main/java/org/linphone/activities/call/OutgoingCallActivity.kt b/app/src/main/java/org/linphone/activities/call/OutgoingCallActivity.kt
index 5c1c9fe82..5dc2a1676 100644
--- a/app/src/main/java/org/linphone/activities/call/OutgoingCallActivity.kt
+++ b/app/src/main/java/org/linphone/activities/call/OutgoingCallActivity.kt
@@ -80,55 +80,58 @@ class OutgoingCallActivity : ProximitySensorActivity() {
binding.controlsViewModel = controlsViewModel
viewModel.callEndedEvent.observe(
- this,
- {
- it.consume {
- Log.i("[Outgoing Call Activity] Call ended, finish activity")
- finish()
- }
+ this
+ ) {
+ it.consume {
+ Log.i("[Outgoing Call Activity] Call ended, finish activity")
+ finish()
}
- )
+ }
viewModel.callConnectedEvent.observe(
- this,
- {
- it.consume {
- Log.i("[Outgoing Call Activity] Call connected, finish activity")
- finish()
- }
+ this
+ ) {
+ it.consume {
+ Log.i("[Outgoing Call Activity] Call connected, finish activity")
+ finish()
}
- )
+ }
controlsViewModel.isSpeakerSelected.observe(
- this,
- {
- enableProximitySensor(!it)
- }
- )
+ this
+ ) {
+ enableProximitySensor(!it)
+ }
- controlsViewModel.askPermissionEvent.observe(
- this,
- {
- it.consume { permission ->
- requestPermissions(arrayOf(permission), 0)
- }
+ controlsViewModel.askAudioRecordPermissionEvent.observe(
+ this
+ ) {
+ it.consume { permission ->
+ requestPermissions(arrayOf(permission), 0)
}
- )
+ }
+
+ controlsViewModel.askCameraPermissionEvent.observe(
+ this
+ ) {
+ it.consume { permission ->
+ requestPermissions(arrayOf(permission), 0)
+ }
+ }
controlsViewModel.toggleNumpadEvent.observe(
- this,
- {
- it.consume { open ->
- if (this::numpadAnimator.isInitialized) {
- if (open) {
- numpadAnimator.start()
- } else {
- numpadAnimator.reverse()
- }
+ this
+ ) {
+ it.consume { open ->
+ if (this::numpadAnimator.isInitialized) {
+ if (open) {
+ numpadAnimator.start()
+ } else {
+ numpadAnimator.reverse()
}
}
}
- )
+ }
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
checkPermissions()
@@ -170,7 +173,7 @@ class OutgoingCallActivity : ProximitySensorActivity() {
Log.i("[Outgoing Call Activity] Asking for RECORD_AUDIO permission")
permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO)
}
- if (viewModel.call.currentParams.videoEnabled() && !PermissionHelper.get().hasCameraPermission()) {
+ if (viewModel.call.currentParams.isVideoEnabled && !PermissionHelper.get().hasCameraPermission()) {
Log.i("[Outgoing Call Activity] Asking for CAMERA permission")
permissionsRequiredList.add(Manifest.permission.CAMERA)
}
@@ -207,7 +210,8 @@ class OutgoingCallActivity : ProximitySensorActivity() {
for (call in coreContext.core.calls) {
if (call.state == Call.State.OutgoingInit ||
call.state == Call.State.OutgoingProgress ||
- call.state == Call.State.OutgoingRinging
+ call.state == Call.State.OutgoingRinging ||
+ call.state == Call.State.OutgoingEarlyMedia
) {
return call
}
diff --git a/app/src/main/java/org/linphone/activities/call/data/CallStatisticsData.kt b/app/src/main/java/org/linphone/activities/call/data/CallStatisticsData.kt
index b99754158..bbfe7a887 100644
--- a/app/src/main/java/org/linphone/activities/call/data/CallStatisticsData.kt
+++ b/app/src/main/java/org/linphone/activities/call/data/CallStatisticsData.kt
@@ -36,7 +36,7 @@ class CallStatisticsData(val call: Call) : GenericContactData(call.remoteAddress
private val listener = object : CoreListenerStub() {
override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) {
if (call == this@CallStatisticsData.call) {
- isVideoEnabled.value = call.currentParams.videoEnabled()
+ isVideoEnabled.value = call.currentParams.isVideoEnabled
updateCallStats(stats)
}
}
@@ -50,7 +50,7 @@ class CallStatisticsData(val call: Call) : GenericContactData(call.remoteAddress
initCallStats()
- val videoEnabled = call.currentParams.videoEnabled()
+ val videoEnabled = call.currentParams.isVideoEnabled
isVideoEnabled.value = videoEnabled
isExpanded.value = coreContext.core.currentCall == call
diff --git a/app/src/main/java/org/linphone/activities/call/fragments/ControlsFragment.kt b/app/src/main/java/org/linphone/activities/call/fragments/ControlsFragment.kt
index 859b47611..3765a4183 100644
--- a/app/src/main/java/org/linphone/activities/call/fragments/ControlsFragment.kt
+++ b/app/src/main/java/org/linphone/activities/call/fragments/ControlsFragment.kt
@@ -88,130 +88,129 @@ class ControlsFragment : GenericFragment() {
binding.conferenceViewModel = conferenceViewModel
callsViewModel.currentCallViewModel.observe(
- viewLifecycleOwner,
- {
- if (it != null) {
- binding.activeCallTimer.base =
- SystemClock.elapsedRealtime() - (1000 * it.call.duration) // Linphone timestamps are in seconds
- binding.activeCallTimer.start()
- }
+ viewLifecycleOwner
+ ) {
+ if (it != null) {
+ binding.activeCallTimer.base =
+ SystemClock.elapsedRealtime() - (1000 * it.call.duration) // Linphone timestamps are in seconds
+ binding.activeCallTimer.start()
}
- )
+ }
callsViewModel.noMoreCallEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- requireActivity().finish()
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ requireActivity().finish()
}
- )
+ }
callsViewModel.askWriteExternalStoragePermissionEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- if (!PermissionHelper.get().hasWriteExternalStoragePermission()) {
- Log.i("[Controls Fragment] Asking for WRITE_EXTERNAL_STORAGE permission")
- requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ if (!PermissionHelper.get().hasWriteExternalStoragePermission()) {
+ Log.i("[Controls Fragment] Asking for WRITE_EXTERNAL_STORAGE permission")
+ requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 2)
}
}
- )
+ }
callsViewModel.callUpdateEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { call ->
- if (call.state == Call.State.StreamsRunning) {
- dialog?.dismiss()
- } else if (call.state == Call.State.UpdatedByRemote) {
- if (coreContext.core.videoCaptureEnabled() || coreContext.core.videoDisplayEnabled()) {
- if (call.currentParams.videoEnabled() != call.remoteParams?.videoEnabled()) {
- showCallVideoUpdateDialog(call)
- }
- } else {
- Log.w("[Controls Fragment] Video display & capture are disabled, don't show video dialog")
+ viewLifecycleOwner
+ ) {
+ it.consume { call ->
+ if (call.state == Call.State.StreamsRunning) {
+ dialog?.dismiss()
+ } else if (call.state == Call.State.UpdatedByRemote) {
+ if (coreContext.core.isVideoCaptureEnabled || coreContext.core.isVideoDisplayEnabled) {
+ if (call.currentParams.isVideoEnabled != call.remoteParams?.isVideoEnabled) {
+ showCallVideoUpdateDialog(call)
}
+ } else {
+ Log.w("[Controls Fragment] Video display & capture are disabled, don't show video dialog")
}
}
}
- )
+ }
controlsViewModel.chatClickedEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- val intent = Intent()
- intent.setClass(requireContext(), MainActivity::class.java)
- intent.putExtra("Chat", true)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- startActivity(intent)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ val intent = Intent()
+ intent.setClass(requireContext(), MainActivity::class.java)
+ intent.putExtra("Chat", true)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(intent)
}
- )
+ }
controlsViewModel.addCallClickedEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- val intent = Intent()
- intent.setClass(requireContext(), MainActivity::class.java)
- intent.putExtra("Dialer", true)
- intent.putExtra("Transfer", false)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- startActivity(intent)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ val intent = Intent()
+ intent.setClass(requireContext(), MainActivity::class.java)
+ intent.putExtra("Dialer", true)
+ intent.putExtra("Transfer", false)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(intent)
}
- )
+ }
controlsViewModel.transferCallClickedEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- val intent = Intent()
- intent.setClass(requireContext(), MainActivity::class.java)
- intent.putExtra("Dialer", true)
- intent.putExtra("Transfer", true)
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- startActivity(intent)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ val intent = Intent()
+ intent.setClass(requireContext(), MainActivity::class.java)
+ intent.putExtra("Dialer", true)
+ intent.putExtra("Transfer", true)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(intent)
}
- )
+ }
- controlsViewModel.askPermissionEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { permission ->
- Log.i("[Controls Fragment] Asking for $permission permission")
- requestPermissions(arrayOf(permission), 0)
- }
+ controlsViewModel.askAudioRecordPermissionEvent.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume { permission ->
+ Log.i("[Controls Fragment] Asking for $permission permission")
+ requestPermissions(arrayOf(permission), 0)
}
- )
+ }
+
+ controlsViewModel.askCameraPermissionEvent.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume { permission ->
+ Log.i("[Controls Fragment] Asking for $permission permission")
+ requestPermissions(arrayOf(permission), 1)
+ }
+ }
controlsViewModel.toggleNumpadEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { open ->
- if (this::numpadAnimator.isInitialized) {
- if (open) {
- numpadAnimator.start()
- } else {
- numpadAnimator.reverse()
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { open ->
+ if (this::numpadAnimator.isInitialized) {
+ if (open) {
+ numpadAnimator.start()
+ } else {
+ numpadAnimator.reverse()
}
}
}
- )
+ }
controlsViewModel.somethingClickedEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- sharedViewModel.resetHiddenInterfaceTimerInVideoCallEvent.value = Event(true)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ sharedViewModel.resetHiddenInterfaceTimerInVideoCallEvent.value = Event(true)
}
- )
+ }
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
checkPermissions()
@@ -251,7 +250,13 @@ class ControlsFragment : GenericFragment() {
}
}
}
- } else if (requestCode == 1 && grantResults.isNotEmpty() && grantResults[0] == PERMISSION_GRANTED) {
+ } else if (requestCode == 1) {
+ if (grantResults.isNotEmpty() && grantResults[0] == PERMISSION_GRANTED) {
+ Log.i("[Controls Fragment] CAMERA permission has been granted")
+ coreContext.core.reloadVideoDevices()
+ controlsViewModel.toggleVideo()
+ }
+ } else if (requestCode == 2 && grantResults.isNotEmpty() && grantResults[0] == PERMISSION_GRANTED) {
callsViewModel.takeScreenshot()
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
diff --git a/app/src/main/java/org/linphone/activities/call/fragments/StatusFragment.kt b/app/src/main/java/org/linphone/activities/call/fragments/StatusFragment.kt
index 0213bb9cf..7837b7482 100644
--- a/app/src/main/java/org/linphone/activities/call/fragments/StatusFragment.kt
+++ b/app/src/main/java/org/linphone/activities/call/fragments/StatusFragment.kt
@@ -64,15 +64,14 @@ class StatusFragment : GenericFragment() {
}
viewModel.showZrtpDialogEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { call ->
- if (call.state == Call.State.Connected || call.state == Call.State.StreamsRunning) {
- showZrtpDialog(call)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { call ->
+ if (call.state == Call.State.Connected || call.state == Call.State.StreamsRunning) {
+ showZrtpDialog(call)
}
}
- )
+ }
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -114,8 +113,8 @@ class StatusFragment : GenericFragment() {
val viewModel = DialogViewModel(getString(R.string.zrtp_dialog_message), getString(R.string.zrtp_dialog_title))
viewModel.showZrtp = true
- viewModel.zrtpReadSas = toRead.toUpperCase(Locale.getDefault())
- viewModel.zrtpListenSas = toListen.toUpperCase(Locale.getDefault())
+ viewModel.zrtpReadSas = toRead.uppercase(Locale.getDefault())
+ viewModel.zrtpListenSas = toListen.uppercase(Locale.getDefault())
viewModel.showIcon = true
viewModel.iconResource = R.drawable.security_2_indicator
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/CallViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/CallViewModel.kt
index d779b5fcc..59216ead9 100644
--- a/app/src/main/java/org/linphone/activities/call/viewmodels/CallViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/call/viewmodels/CallViewModel.kt
@@ -114,6 +114,7 @@ open class CallViewModel(val call: Call) : GenericContactViewModel(call.remoteAd
call.addListener(listener)
isPaused.value = call.state == Call.State.Paused
+ isOutgoingEarlyMedia.value = call.state == Call.State.OutgoingEarlyMedia
}
override fun onCleared() {
@@ -138,7 +139,7 @@ open class CallViewModel(val call: Call) : GenericContactViewModel(call.remoteAd
}
fun takeScreenshot() {
- if (call.currentParams.videoEnabled()) {
+ if (call.currentParams.isVideoEnabled) {
val fileName = System.currentTimeMillis().toString() + ".jpeg"
call.takeVideoSnapshot(FileUtils.getFileStoragePath(fileName).absolutePath)
}
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/CallsViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/CallsViewModel.kt
index 2372b756d..e6165e651 100644
--- a/app/src/main/java/org/linphone/activities/call/viewmodels/CallsViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/call/viewmodels/CallsViewModel.kt
@@ -75,11 +75,11 @@ class CallsViewModel : ViewModel() {
} else if (call.state == Call.State.UpdatedByRemote) {
// If the correspondent asks to turn on video while audio call,
// defer update until user has chosen whether to accept it or not
- val remoteVideo = call.remoteParams?.videoEnabled() ?: false
- val localVideo = call.currentParams.videoEnabled()
+ val remoteVideo = call.remoteParams?.isVideoEnabled ?: false
+ val localVideo = call.currentParams.isVideoEnabled
val autoAccept = call.core.videoActivationPolicy.automaticallyAccept
if (remoteVideo && !localVideo && !autoAccept) {
- if (coreContext.core.videoCaptureEnabled() || coreContext.core.videoDisplayEnabled()) {
+ if (coreContext.core.isVideoCaptureEnabled || coreContext.core.isVideoDisplayEnabled) {
call.deferUpdate()
callUpdateEvent.value = Event(call)
} else {
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsViewModel.kt
index f74bb0471..094cd64c2 100644
--- a/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsViewModel.kt
@@ -87,7 +87,11 @@ class ControlsViewModel : ViewModel() {
MutableLiveData>()
}
- val askPermissionEvent: MutableLiveData> by lazy {
+ val askAudioRecordPermissionEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val askCameraPermissionEvent: MutableLiveData> by lazy {
MutableLiveData>()
}
@@ -176,7 +180,7 @@ class ControlsViewModel : ViewModel() {
}
if (coreContext.isVideoCallOrConferenceActive() && !PermissionHelper.get().hasCameraPermission()) {
- askPermissionEvent.value = Event(Manifest.permission.CAMERA)
+ askCameraPermissionEvent.value = Event(Manifest.permission.CAMERA)
}
updateUI()
@@ -244,13 +248,13 @@ class ControlsViewModel : ViewModel() {
fun toggleMuteMicrophone() {
if (!PermissionHelper.get().hasRecordAudioPermission()) {
- askPermissionEvent.value = Event(Manifest.permission.RECORD_AUDIO)
+ askAudioRecordPermissionEvent.value = Event(Manifest.permission.RECORD_AUDIO)
return
}
somethingClickedEvent.value = Event(true)
- val micEnabled = coreContext.core.micEnabled()
- coreContext.core.enableMic(!micEnabled)
+ val micEnabled = coreContext.core.isMicEnabled
+ coreContext.core.isMicEnabled = !micEnabled
updateMuteMicState()
}
@@ -279,7 +283,7 @@ class ControlsViewModel : ViewModel() {
fun toggleVideo() {
if (!PermissionHelper.get().hasCameraPermission()) {
- askPermissionEvent.value = Event(Manifest.permission.CAMERA)
+ askCameraPermissionEvent.value = Event(Manifest.permission.CAMERA)
return
}
@@ -300,7 +304,7 @@ class ControlsViewModel : ViewModel() {
isVideoUpdateInProgress.value = true
val params = core.createCallParams(currentCall)
- params?.enableVideo(!currentCall.currentParams.videoEnabled())
+ params?.isVideoEnabled = !currentCall.currentParams.isVideoEnabled
currentCall.update(params)
}
}
@@ -338,23 +342,27 @@ class ControlsViewModel : ViewModel() {
val currentCall = core.currentCall
val conference = core.conference
- if (currentCall != null) {
- if (currentCall.isRecording) {
- currentCall.stopRecording()
- } else {
- currentCall.startRecording()
+ when {
+ currentCall != null -> {
+ if (currentCall.isRecording) {
+ currentCall.stopRecording()
+ } else {
+ currentCall.startRecording()
+ }
+ isRecording.value = currentCall.isRecording
}
- isRecording.value = currentCall.isRecording
- } else if (conference != null) {
- val path = LinphoneUtils.getRecordingFilePathForConference()
- if (conference.isRecording) {
- conference.stopRecording()
- } else {
- conference.startRecording(path)
+ conference != null -> {
+ val path = LinphoneUtils.getRecordingFilePathForConference()
+ if (conference.isRecording) {
+ conference.stopRecording()
+ } else {
+ conference.startRecording(path)
+ }
+ isRecording.value = conference.isRecording
+ }
+ else -> {
+ isRecording.value = false
}
- isRecording.value = conference.isRecording
- } else {
- isRecording.value = false
}
if (closeMenu) toggleOptionsMenu()
@@ -378,7 +386,7 @@ class ControlsViewModel : ViewModel() {
somethingClickedEvent.value = Event(true)
val core = coreContext.core
- val currentCallVideoEnabled = core.currentCall?.currentParams?.videoEnabled() ?: false
+ val currentCallVideoEnabled = core.currentCall?.currentParams?.isVideoEnabled ?: false
val params = core.createConferenceParams()
params.isVideoEnabled = currentCallVideoEnabled
@@ -411,7 +419,7 @@ class ControlsViewModel : ViewModel() {
}
fun updateMuteMicState() {
- isMicrophoneMuted.value = !PermissionHelper.get().hasRecordAudioPermission() || !coreContext.core.micEnabled()
+ isMicrophoneMuted.value = !PermissionHelper.get().hasRecordAudioPermission() || !coreContext.core.isMicEnabled
isMuteMicrophoneEnabled.value = coreContext.core.currentCall != null || coreContext.core.conference?.isIn == true
}
@@ -457,7 +465,7 @@ class ControlsViewModel : ViewModel() {
private fun updateVideoAvailable() {
val core = coreContext.core
val currentCall = core.currentCall
- isVideoAvailable.value = (core.videoCaptureEnabled() || core.videoPreviewEnabled()) &&
+ isVideoAvailable.value = (core.isVideoCaptureEnabled || core.isVideoPreviewEnabled) &&
(
(currentCall != null && !currentCall.mediaInProgress()) ||
core.conference?.isIn == true
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/IncomingCallViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/IncomingCallViewModel.kt
index 639a43a22..f1dc0bfb6 100644
--- a/app/src/main/java/org/linphone/activities/call/viewmodels/IncomingCallViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/call/viewmodels/IncomingCallViewModel.kt
@@ -60,10 +60,10 @@ class IncomingCallViewModel(call: Call) : CallViewModel(call) {
coreContext.core.addListener(listener)
screenLocked.value = false
- inviteWithVideo.value = call.remoteParams?.videoEnabled() == true && coreContext.core.videoActivationPolicy.automaticallyAccept
+ inviteWithVideo.value = call.remoteParams?.isVideoEnabled == true && coreContext.core.videoActivationPolicy.automaticallyAccept
earlyMediaVideoEnabled.value = corePreferences.acceptEarlyMedia &&
call.state == Call.State.IncomingEarlyMedia &&
- call.currentParams.videoEnabled()
+ call.currentParams.isVideoEnabled
}
override fun onCleared() {
diff --git a/app/src/main/java/org/linphone/activities/chat_bubble/ChatBubbleActivity.kt b/app/src/main/java/org/linphone/activities/chat_bubble/ChatBubbleActivity.kt
index 0169412cc..08d3d8d1e 100644
--- a/app/src/main/java/org/linphone/activities/chat_bubble/ChatBubbleActivity.kt
+++ b/app/src/main/java/org/linphone/activities/chat_bubble/ChatBubbleActivity.kt
@@ -37,6 +37,8 @@ import org.linphone.activities.main.chat.adapters.ChatMessagesListAdapter
import org.linphone.activities.main.chat.viewmodels.*
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
import org.linphone.core.ChatRoom
+import org.linphone.core.ChatRoomListenerStub
+import org.linphone.core.EventLog
import org.linphone.core.Factory
import org.linphone.core.tools.Log
import org.linphone.databinding.ChatBubbleActivityBinding
@@ -58,6 +60,12 @@ class ChatBubbleActivity : GenericActivity() {
}
}
+ private val listener = object : ChatRoomListenerStub() {
+ override fun onChatMessageReceived(chatRoom: ChatRoom, eventLog: EventLog) {
+ chatRoom.markAsRead()
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -86,11 +94,6 @@ class ChatBubbleActivity : GenericActivity() {
return
}
- // Workaround for the removed notification when a chat room is marked as read
- coreContext.notificationsManager.dismissNotificationUponReadChatRoom = false
- chatRoom.markAsRead()
- coreContext.notificationsManager.dismissNotificationUponReadChatRoom = true
-
viewModel = ViewModelProvider(
this,
ChatRoomViewModelFactory(chatRoom)
@@ -119,38 +122,40 @@ class ChatBubbleActivity : GenericActivity() {
adapter.disableContextMenu()
adapter.openContentEvent.observe(
- this,
- {
- it.consume { content ->
- if (content.isFileEncrypted) {
- Toast.makeText(this, R.string.chat_bubble_cant_open_enrypted_file, Toast.LENGTH_LONG).show()
- } else {
- FileUtils.openFileInThirdPartyApp(this, content.filePath.orEmpty(), true)
- }
+ this
+ ) {
+ it.consume { content ->
+ if (content.isFileEncrypted) {
+ Toast.makeText(
+ this,
+ R.string.chat_bubble_cant_open_enrypted_file,
+ Toast.LENGTH_LONG
+ ).show()
+ } else {
+ FileUtils.openFileInThirdPartyApp(this, content.filePath.orEmpty(), true)
}
}
- )
+ }
val layoutManager = LinearLayoutManager(this)
layoutManager.stackFromEnd = true
binding.chatMessagesList.layoutManager = layoutManager
listViewModel.events.observe(
- this,
- { events ->
- adapter.submitList(events)
- }
- )
+ this
+ ) { events ->
+ adapter.submitList(events)
+ }
chatSendingViewModel.textToSend.observe(
- this,
- {
- chatSendingViewModel.onTextToSendChanged(it)
- }
- )
+ this
+ ) {
+ chatSendingViewModel.onTextToSendChanged(it)
+ }
binding.setOpenAppClickListener {
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
+ coreContext.notificationsManager.changeDismissNotificationUponReadForChatRoom(viewModel.chatRoom, false)
val intent = Intent(this, MainActivity::class.java)
intent.putExtra("RemoteSipUri", remoteSipUri)
@@ -173,6 +178,12 @@ class ChatBubbleActivity : GenericActivity() {
override fun onResume() {
super.onResume()
+ viewModel.chatRoom.addListener(listener)
+
+ // Workaround for the removed notification when a chat room is marked as read
+ coreContext.notificationsManager.changeDismissNotificationUponReadForChatRoom(viewModel.chatRoom, true)
+ viewModel.chatRoom.markAsRead()
+
val peerAddress = viewModel.chatRoom.peerAddress.asStringUriOnly()
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = peerAddress
coreContext.notificationsManager.resetChatNotificationCounterForSipUri(peerAddress)
@@ -185,7 +196,10 @@ class ChatBubbleActivity : GenericActivity() {
}
override fun onPause() {
+ viewModel.chatRoom.removeListener(listener)
+
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
+ coreContext.notificationsManager.changeDismissNotificationUponReadForChatRoom(viewModel.chatRoom, false)
super.onPause()
}
diff --git a/app/src/main/java/org/linphone/activities/launcher/LauncherActivity.kt b/app/src/main/java/org/linphone/activities/launcher/LauncherActivity.kt
deleted file mode 100644
index d8a206adf..000000000
--- a/app/src/main/java/org/linphone/activities/launcher/LauncherActivity.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (c) 2010-2020 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.linphone.activities.launcher
-
-import android.content.Intent
-import android.os.Bundle
-import org.linphone.LinphoneApplication.Companion.coreContext
-import org.linphone.LinphoneApplication.Companion.corePreferences
-import org.linphone.R
-import org.linphone.activities.GenericActivity
-import org.linphone.activities.main.MainActivity
-import org.linphone.core.tools.Log
-
-class LauncherActivity : GenericActivity() {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setContentView(R.layout.launcher_activity)
- }
-
- override fun onStart() {
- super.onStart()
- coreContext.handler.postDelayed({ onReady() }, 500)
- }
-
- private fun onReady() {
- Log.i("[Launcher] Core is ready")
-
- if (corePreferences.preventInterfaceFromShowingUp) {
- Log.w("[Context] We were asked to not show the user interface")
- finish()
- return
- }
-
- val intent = Intent()
- intent.setClass(this, MainActivity::class.java)
-
- // Propagate current intent action, type and data
- if (getIntent() != null) {
- val extras = getIntent().extras
- if (extras != null) intent.putExtras(extras)
- }
- intent.action = getIntent().action
- intent.type = getIntent().type
- intent.data = getIntent().data
-
- startActivity(intent)
- if (corePreferences.enableAnimations) {
- overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
- }
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/main/MainActivity.kt b/app/src/main/java/org/linphone/activities/main/MainActivity.kt
index bd2aabd1e..b995f5371 100644
--- a/app/src/main/java/org/linphone/activities/main/MainActivity.kt
+++ b/app/src/main/java/org/linphone/activities/main/MainActivity.kt
@@ -30,8 +30,10 @@ import android.view.Gravity
import android.view.MotionEvent
import android.view.View
import android.view.inputmethod.InputMethodManager
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.doOnAttach
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.ViewModelProvider
@@ -48,8 +50,7 @@ import kotlinx.coroutines.*
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
-import org.linphone.activities.GenericActivity
-import org.linphone.activities.SnackBarActivity
+import org.linphone.activities.*
import org.linphone.activities.assistant.AssistantActivity
import org.linphone.activities.main.viewmodels.CallOverlayViewModel
import org.linphone.activities.main.viewmodels.SharedMainViewModel
@@ -112,6 +113,8 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ val splashScreen = installSplashScreen()
+
binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
binding.lifecycleOwner = this
@@ -122,30 +125,27 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
binding.callOverlayViewModel = callOverlayViewModel
sharedViewModel.toggleDrawerEvent.observe(
- this,
- {
- it.consume {
- if (binding.sideMenu.isDrawerOpen(Gravity.LEFT)) {
- binding.sideMenu.closeDrawer(binding.sideMenuContent, true)
- } else {
- binding.sideMenu.openDrawer(binding.sideMenuContent, true)
- }
+ this
+ ) {
+ it.consume {
+ if (binding.sideMenu.isDrawerOpen(Gravity.LEFT)) {
+ binding.sideMenu.closeDrawer(binding.sideMenuContent, true)
+ } else {
+ binding.sideMenu.openDrawer(binding.sideMenuContent, true)
}
}
- )
+ }
coreContext.callErrorMessageResourceId.observe(
- this,
- {
- it.consume { message ->
- showSnackBar(message)
- }
+ this
+ ) {
+ it.consume { message ->
+ showSnackBar(message)
}
- )
+ }
if (coreContext.core.accountList.isEmpty()) {
if (corePreferences.firstStart) {
- corePreferences.firstStart = false
startActivity(Intent(this, AssistantActivity::class.java))
}
}
@@ -153,7 +153,14 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
tabsFragment = findViewById(R.id.tabs_fragment)
statusFragment = findViewById(R.id.status_fragment)
- initOverlay()
+ binding.root.doOnAttach {
+ Log.i("[Main Activity] Report UI has been fully drawn (TTFD)")
+ try {
+ reportFullyDrawn()
+ } catch (se: SecurityException) {
+ Log.e("[Main Activity] Security exception when doing reportFullyDrawn(): $se")
+ }
+ }
}
override fun onNewIntent(intent: Intent?) {
@@ -176,6 +183,16 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
Snackbar.make(findViewById(R.id.coordinator), resourceId, Snackbar.LENGTH_LONG).show()
}
+ override fun showSnackBar(resourceId: Int, action: Int, listener: () -> Unit) {
+ Snackbar
+ .make(findViewById(R.id.coordinator), resourceId, Snackbar.LENGTH_LONG)
+ .setAction(action) {
+ Log.i("[Snack Bar] Action listener triggered")
+ listener()
+ }
+ .show()
+ }
+
override fun showSnackBar(message: String) {
Snackbar.make(findViewById(R.id.coordinator), message, Snackbar.LENGTH_LONG).show()
}
@@ -195,6 +212,8 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
updateTabsFragmentVisibility()
}
+ initOverlay()
+
if (intent != null) handleIntentParams(intent)
}
@@ -209,7 +228,7 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
destination: NavDestination,
arguments: Bundle?
) {
- currentFocus?.hideKeyboard()
+ hideKeyboard()
if (statusFragment.visibility == View.GONE) {
statusFragment.visibility = View.VISIBLE
}
@@ -222,6 +241,10 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
updateTabsFragmentVisibility()
}
+ fun hideKeyboard() {
+ currentFocus?.hideKeyboard()
+ }
+
private fun updateTabsFragmentVisibility() {
tabsFragment.visibility = if (tabsFragmentVisible1 && tabsFragmentVisible2) View.VISIBLE else View.GONE
}
@@ -253,9 +276,8 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
if (uri != null) {
val contactId = coreContext.contactsManager.getAndroidContactIdFromUri(uri)
if (contactId != null) {
- val deepLink = "linphone-android://contact/view/$contactId"
- Log.i("[Main Activity] Found contact URI parameter in intent: $uri, starting deep link: $deepLink")
- findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
+ Log.i("[Main Activity] Found contact URI parameter in intent: $uri")
+ navigateToContact(contactId)
}
}
} else {
@@ -282,9 +304,8 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
when {
intent.hasExtra("ContactId") -> {
val id = intent.getStringExtra("ContactId")
- val deepLink = "linphone-android://contact/view/$id"
- Log.i("[Main Activity] Found contact id parameter in intent: $id, starting deep link: $deepLink")
- findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
+ Log.i("[Main Activity] Found contact ID in extras: $id")
+ navigateToContact(id)
}
intent.hasExtra("Chat") -> {
if (corePreferences.disableChat) return
@@ -293,10 +314,10 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
val peerAddress = intent.getStringExtra("RemoteSipUri")
val localAddress = intent.getStringExtra("LocalSipUri")
Log.i("[Main Activity] Found chat room intent extra: local SIP URI=[$localAddress], peer SIP URI=[$peerAddress]")
- findNavController(R.id.nav_host_fragment).navigate(Uri.parse("linphone-android://chat-room/$localAddress/$peerAddress"))
+ navigateToChatRoom(localAddress, peerAddress)
} else {
Log.i("[Main Activity] Found chat intent extra, go to chat rooms list")
- findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_masterChatRoomsFragment)
+ navigateToChatRooms()
}
}
intent.hasExtra("Dialer") -> {
@@ -427,12 +448,11 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
addressToIM = addressToIM.substring("mmsto:".length)
}
- val peerAddress = coreContext.core.interpretUrl(addressToIM)?.asStringUriOnly()
val localAddress =
coreContext.core.defaultAccount?.params?.identityAddress?.asStringUriOnly()
- val deepLink = "linphone-android://chat-room/$localAddress/$peerAddress"
- Log.i("[Main Activity] Starting deep link: $deepLink")
- findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
+ val peerAddress = coreContext.core.interpretUrl(addressToIM)?.asStringUriOnly()
+ Log.i("[Main Activity] Navigating to chat room with local [$localAddress] and peer [$peerAddress] addresses")
+ navigateToChatRoom(localAddress, peerAddress)
} else {
val shortcutId = intent.getStringExtra("android.intent.extra.shortcut.ID") // Intent.EXTRA_SHORTCUT_ID
if (shortcutId != null) {
@@ -440,7 +460,7 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
handleLocusOrShortcut(shortcutId)
} else {
Log.i("[Main Activity] Going into chat rooms list")
- findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_masterChatRoomsFragment)
+ navigateToChatRooms()
}
}
}
@@ -450,11 +470,11 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
if (split.size == 2) {
val localAddress = split[0]
val peerAddress = split[1]
- val deepLink = "linphone-android://chat-room/$localAddress/$peerAddress"
- findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
+ Log.i("[Main Activity] Navigating to chat room with local [$localAddress] and peer [$peerAddress] addresses, computed from shortcut/locus id")
+ navigateToChatRoom(localAddress, peerAddress)
} else {
- Log.e("[Main Activity] Failed to parse shortcut/locus id: $id")
- findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_masterChatRoomsFragment)
+ Log.e("[Main Activity] Failed to parse shortcut/locus id: $id, going to chat rooms list")
+ navigateToChatRooms()
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/about/AboutFragment.kt b/app/src/main/java/org/linphone/activities/main/about/AboutFragment.kt
index 497c26f95..bb3ff4969 100644
--- a/app/src/main/java/org/linphone/activities/main/about/AboutFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/about/AboutFragment.kt
@@ -58,5 +58,13 @@ class AboutFragment : SecureFragment() {
)
startActivity(browserIntent)
}
+
+ binding.setWeblateClickListener {
+ val browserIntent = Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(getString(R.string.about_weblate_link))
+ )
+ startActivity(browserIntent)
+ }
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/ChatScrollListener.kt b/app/src/main/java/org/linphone/activities/main/chat/ChatScrollListener.kt
index 440f1d239..d2e2c7622 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/ChatScrollListener.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/ChatScrollListener.kt
@@ -29,7 +29,7 @@ internal abstract class ChatScrollListener(private val mLayoutManager: LinearLay
// True if we are still waiting for the last set of data to load.
private var loading = true
- var userHasScrolledUp: Boolean = false
+ private var userHasScrolledUp: Boolean = false
// This happens many times a second during a scroll, so be wary of the code you place here.
// We are given a few useful parameters to help us work out if we need to load some more data,
diff --git a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt
index 676be21b0..12a69892f 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt
@@ -86,6 +86,10 @@ class ChatMessagesListAdapter(
MutableLiveData>()
}
+ val sipUriClickedEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
val scrollToChatMessageEvent: MutableLiveData> by lazy {
MutableLiveData>()
}
@@ -94,6 +98,10 @@ class ChatMessagesListAdapter(
override fun onContentClicked(content: Content) {
openContentEvent.value = Event(content)
}
+
+ override fun onSipAddressClicked(sipUri: String) {
+ sipUriClickedEvent.value = Event(sipUri)
+ }
}
private var contextMenuDisabled: Boolean = false
@@ -211,15 +219,14 @@ class ChatMessagesListAdapter(
// This is for item selection through ListTopBarFragment
selectionListViewModel = selectionViewModel
selectionViewModel.isEditionEnabled.observe(
- viewLifecycleOwner,
- {
- position = adapterPosition
- }
- )
+ viewLifecycleOwner
+ ) {
+ position = bindingAdapterPosition
+ }
setClickListener {
if (selectionViewModel.isEditionEnabled.value == true) {
- selectionViewModel.onToggleSelect(adapterPosition)
+ selectionViewModel.onToggleSelect(bindingAdapterPosition)
}
}
@@ -234,8 +241,8 @@ class ChatMessagesListAdapter(
var hasPrevious = false
var hasNext = false
- if (adapterPosition > 0) {
- val previousItem = getItem(adapterPosition - 1)
+ if (bindingAdapterPosition > 0) {
+ val previousItem = getItem(bindingAdapterPosition - 1)
if (previousItem.eventLog.type == EventLog.Type.ConferenceChatMessage) {
val previousMessage = previousItem.eventLog.chatMessage
if (previousMessage != null && previousMessage.fromAddress.weakEqual(chatMessage.fromAddress)) {
@@ -246,8 +253,8 @@ class ChatMessagesListAdapter(
}
}
- if (adapterPosition >= 0 && adapterPosition < itemCount - 1) {
- val nextItem = getItem(adapterPosition + 1)
+ if (bindingAdapterPosition >= 0 && bindingAdapterPosition < itemCount - 1) {
+ val nextItem = getItem(bindingAdapterPosition + 1)
if (nextItem.eventLog.type == EventLog.Type.ConferenceChatMessage) {
val nextMessage = nextItem.eventLog.chatMessage
if (nextMessage != null && nextMessage.fromAddress.weakEqual(chatMessage.fromAddress)) {
@@ -272,9 +279,8 @@ class ChatMessagesListAdapter(
val itemSize = AppUtils.getDimension(R.dimen.chat_message_popup_item_height).toInt()
var totalSize = itemSize * 7
- if (chatMessage.chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) ||
- chatMessage.state == ChatMessage.State.NotDelivered
- ) { // No message id
+ if (chatMessage.chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) {
+ // No message id
popupView.imdnHidden = true
totalSize -= itemSize
}
@@ -347,7 +353,7 @@ class ChatMessagesListAdapter(
private fun resendMessage() {
val chatMessage = binding.data?.chatMessage
if (chatMessage != null) {
- chatMessage.userData = adapterPosition
+ chatMessage.userData = bindingAdapterPosition
resendMessageEvent.value = Event(chatMessage)
}
}
@@ -389,7 +395,7 @@ class ChatMessagesListAdapter(
private fun deleteMessage() {
val chatMessage = binding.data?.chatMessage
if (chatMessage != null) {
- chatMessage.userData = adapterPosition
+ chatMessage.userData = bindingAdapterPosition
deleteMessageEvent.value = Event(chatMessage)
}
}
@@ -417,15 +423,14 @@ class ChatMessagesListAdapter(
// This is for item selection through ListTopBarFragment
selectionListViewModel = selectionViewModel
selectionViewModel.isEditionEnabled.observe(
- viewLifecycleOwner,
- {
- position = adapterPosition
- }
- )
+ viewLifecycleOwner
+ ) {
+ position = bindingAdapterPosition
+ }
binding.setClickListener {
if (selectionViewModel.isEditionEnabled.value == true) {
- selectionViewModel.onToggleSelect(adapterPosition)
+ selectionViewModel.onToggleSelect(bindingAdapterPosition)
}
}
@@ -452,8 +457,18 @@ private class ChatMessageDiffCallback : DiffUtil.ItemCallback() {
oldItem: EventLogData,
newItem: EventLogData
): Boolean {
- return if (newItem.eventLog.type == EventLog.Type.ConferenceChatMessage) {
- newItem.eventLog.chatMessage?.state == ChatMessage.State.Displayed
- } else true
+ return if (oldItem.eventLog.type == EventLog.Type.ConferenceChatMessage &&
+ newItem.eventLog.type == EventLog.Type.ConferenceChatMessage
+ ) {
+ val oldData = (oldItem.data as ChatMessageData)
+ val newData = (newItem.data as ChatMessageData)
+
+ val previous = oldData.hasPreviousMessage == newData.hasPreviousMessage
+ val next = oldData.hasNextMessage == newData.hasNextMessage
+ newItem.eventLog.chatMessage?.state == ChatMessage.State.Displayed && previous && next
+ } else {
+ oldItem.eventLog.type != EventLog.Type.ConferenceChatMessage &&
+ newItem.eventLog.type != EventLog.Type.ConferenceChatMessage
+ }
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomsListAdapter.kt b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomsListAdapter.kt
index d99eacb7d..18face997 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomsListAdapter.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomsListAdapter.kt
@@ -73,17 +73,16 @@ class ChatRoomsListAdapter(
// This is for item selection through ListTopBarFragment
selectionListViewModel = selectionViewModel
selectionViewModel.isEditionEnabled.observe(
- viewLifecycleOwner,
- {
- position = adapterPosition
- }
- )
+ viewLifecycleOwner
+ ) {
+ position = bindingAdapterPosition
+ }
forwardPending = isForwardPending
setClickListener {
if (selectionViewModel.isEditionEnabled.value == true) {
- selectionViewModel.onToggleSelect(adapterPosition)
+ selectionViewModel.onToggleSelect(bindingAdapterPosition)
} else {
selectedChatRoomEvent.value = Event(chatRoomViewModel.chatRoom)
}
@@ -116,6 +115,6 @@ private class ChatRoomDiffCallback : DiffUtil.ItemCallback()
oldItem: ChatRoomViewModel,
newItem: ChatRoomViewModel
): Boolean {
- return newItem.unreadMessagesCount.value == 0
+ return false // To force redraw when contacts are updated
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt
index 0623cf4ac..8c81a62cf 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt
@@ -44,7 +44,6 @@ import org.linphone.utils.ImageUtils
class ChatMessageContentData(
private val chatMessage: ChatMessage,
private val contentIndex: Int,
-
) {
var listener: OnContentClickedListener? = null
@@ -60,7 +59,6 @@ class ChatMessageContentData(
val fileName = MutableLiveData()
val filePath = MutableLiveData()
- val fileSize = MutableLiveData()
val downloadable = MutableLiveData()
val downloadEnabled = MutableLiveData()
@@ -72,13 +70,11 @@ class ChatMessageContentData(
val formattedDuration = MutableLiveData()
val voiceRecordPlayingPosition = MutableLiveData()
val isVoiceRecordPlaying = MutableLiveData()
- var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
val isAlone: Boolean
get() {
var count = 0
for (content in chatMessage.contents) {
- val content = getContent()
if (content.isFileTransfer || content.isFile) {
count += 1
}
@@ -86,7 +82,9 @@ class ChatMessageContentData(
return count == 1
}
- var isFileEncrypted: Boolean = false
+ private var isFileEncrypted: Boolean = false
+
+ private var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
private lateinit var voiceRecordingPlayer: Player
private val playerListener = PlayerListener {
@@ -145,13 +143,7 @@ class ChatMessageContentData(
fun destroy() {
scope.cancel()
- val path = filePath.value.orEmpty()
- if (path.isNotEmpty() && isFileEncrypted) {
- Log.i("[Content] Deleting file used for preview: $path")
- FileUtils.deleteFile(path)
- filePath.value = ""
- }
-
+ deletePlainFilePath()
chatMessage.removeListener(chatMessageListener)
if (this::voiceRecordingPlayer.isInitialized) {
@@ -181,9 +173,22 @@ class ChatMessageContentData(
listener?.onContentClicked(getContent())
}
+ private fun deletePlainFilePath() {
+ val path = filePath.value.orEmpty()
+ if (path.isNotEmpty() && isFileEncrypted) {
+ Log.i("[Content] Deleting file used for preview: $path")
+ FileUtils.deleteFile(path)
+ filePath.value = ""
+ }
+ }
+
private fun updateContent() {
+ Log.i("[Content] Updating content")
+ deletePlainFilePath()
+
val content = getContent()
isFileEncrypted = content.isFileEncrypted
+ Log.i("[Content] Is ${if (content.isFile) "file" else "file transfer"} content encrypted ? $isFileEncrypted")
filePath.value = ""
fileName.value = if (content.name.isNullOrEmpty() && !content.filePath.isNullOrEmpty()) {
@@ -193,14 +198,18 @@ class ChatMessageContentData(
}
// Display download size and underline text
- fileSize.value = AppUtils.bytesToDisplayableSize(content.fileSize.toLong())
- val spannable = SpannableString("${AppUtils.getString(R.string.chat_message_download_file)} (${fileSize.value})")
+ val fileSize = AppUtils.bytesToDisplayableSize(content.fileSize.toLong())
+ val spannable = SpannableString("${AppUtils.getString(R.string.chat_message_download_file)} ($fileSize)")
spannable.setSpan(UnderlineSpan(), 0, spannable.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
downloadLabel.value = spannable
if (content.isFile || (content.isFileTransfer && chatMessage.isOutgoing)) {
- Log.i("[Content] Is content encrypted ? $isFileEncrypted")
- val path = if (isFileEncrypted) content.plainFilePath else content.filePath ?: ""
+ val path = if (isFileEncrypted) {
+ Log.i("[Content] Content is encrypted, requesting plain file path")
+ content.plainFilePath
+ } else {
+ content.filePath ?: ""
+ }
downloadable.value = content.filePath.orEmpty().isEmpty()
if (path.isNotEmpty()) {
@@ -226,7 +235,7 @@ class ChatMessageContentData(
}
}
} else {
- Log.w("[Content] Found content with empty path...")
+ Log.w("[Content] Found ${if (content.isFile) "file" else "file transfer"} content with empty path...")
isImage.value = false
isVideo.value = false
isAudio.value = false
@@ -297,8 +306,9 @@ class ChatMessageContentData(
private fun initVoiceRecordPlayer() {
Log.i("[Voice Recording] Creating player for voice record")
- // Use speaker sound card to play recordings, otherwise use earpiece
+ // In case no headphones/headset is connected, use speaker sound card to play recordings, otherwise use earpiece
// If none are available, default one will be used
+ var headphonesCard: String? = null
var speakerCard: String? = null
var earpieceCard: String? = null
for (device in coreContext.core.audioDevices) {
@@ -307,12 +317,14 @@ class ChatMessageContentData(
speakerCard = device.id
} else if (device.type == AudioDevice.Type.Earpiece) {
earpieceCard = device.id
+ } else if (device.type == AudioDevice.Type.Headphones || device.type == AudioDevice.Type.Headset) {
+ headphonesCard = device.id
}
}
}
- Log.i("[Voice Recording] Found speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]")
+ Log.i("[Voice Recording] Found headset/headphones sound card [$headphonesCard], speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]")
- val localPlayer = coreContext.core.createLocalPlayer(speakerCard ?: earpieceCard, null, null)
+ val localPlayer = coreContext.core.createLocalPlayer(headphonesCard ?: speakerCard ?: earpieceCard, null, null)
if (localPlayer != null) {
voiceRecordingPlayer = localPlayer
} else {
@@ -321,8 +333,7 @@ class ChatMessageContentData(
}
voiceRecordingPlayer.addListener(playerListener)
- val content = getContent()
- val path = if (content.isFileEncrypted) content.plainFilePath else content.filePath ?: ""
+ val path = filePath.value
voiceRecordingPlayer.open(path.orEmpty())
voiceRecordDuration.value = voiceRecordingPlayer.duration
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(voiceRecordingPlayer.duration) // is already in milliseconds
@@ -346,4 +357,6 @@ class ChatMessageContentData(
interface OnContentClickedListener {
fun onContentClicked(content: Content)
+
+ fun onSipAddressClicked(sipUri: String)
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt
index cd8230096..3f588d008 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt
@@ -24,12 +24,14 @@ import android.text.Spannable
import android.text.util.Linkify
import androidx.core.text.util.LinkifyCompat
import androidx.lifecycle.MutableLiveData
+import java.util.regex.Pattern
import org.linphone.R
import org.linphone.contact.GenericContactData
import org.linphone.core.ChatMessage
import org.linphone.core.ChatMessageListenerStub
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
+import org.linphone.utils.PatternClickableSpan
import org.linphone.utils.TimestampUtils
class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMessage.fromAddress) {
@@ -59,6 +61,9 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
val replyData = MutableLiveData()
+ var hasPreviousMessage = false
+ var hasNextMessage = false
+
private var countDownTimer: CountDownTimer? = null
private val listener = object : ChatMessageListenerStub() {
@@ -106,6 +111,11 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
}
fun updateBubbleBackground(hasPrevious: Boolean, hasNext: Boolean) {
+ hasPreviousMessage = hasPrevious
+ hasNextMessage = hasNext
+ hideTime.value = false
+ hideAvatar.value = false
+
if (hasPrevious) {
hideTime.value = true
}
@@ -165,16 +175,25 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
val list = arrayListOf()
val contentsList = chatMessage.contents
- for (index in 0 until contentsList.size) {
+ for (index in contentsList.indices) {
val content = contentsList[index]
if (content.isFileTransfer || content.isFile) {
val data = ChatMessageContentData(chatMessage, index)
data.listener = contentListener
list.add(data)
} else if (content.isText) {
- val spannable = Spannable.Factory.getInstance().newSpannable(content.utf8Text)
+ val spannable = Spannable.Factory.getInstance().newSpannable(content.utf8Text?.trim())
LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS)
- text.value = spannable
+ text.value = PatternClickableSpan()
+ .add(
+ Pattern.compile("(sips?):([^@]+)(?:@([^ ]+))?"),
+ object : PatternClickableSpan.SpannableClickedListener {
+ override fun onSpanClicked(text: String) {
+ Log.i("[Chat Message Data] Clicked on SIP URI: $text")
+ contentListener?.onSipAddressClicked(text)
+ }
+ }
+ ).build(spannable)
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt
index 714f8834d..95b08e3bf 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt
@@ -93,62 +93,55 @@ class ChatRoomCreationFragment : SecureFragment
}
viewModel.contactsList.observe(
- viewLifecycleOwner,
- {
- adapter.submitList(it)
- }
- )
+ viewLifecycleOwner
+ ) {
+ adapter.submitList(it)
+ }
viewModel.isEncrypted.observe(
- viewLifecycleOwner,
- {
- adapter.updateSecurity(it)
- }
- )
+ viewLifecycleOwner
+ ) {
+ adapter.updateSecurity(it)
+ }
viewModel.sipContactsSelected.observe(
- viewLifecycleOwner,
- {
- viewModel.updateContactsList()
- }
- )
+ viewLifecycleOwner
+ ) {
+ viewModel.updateContactsList()
+ }
viewModel.selectedAddresses.observe(
- viewLifecycleOwner,
- {
- adapter.updateSelectedAddresses(it)
- }
- )
+ viewLifecycleOwner
+ ) {
+ adapter.updateSelectedAddresses(it)
+ }
viewModel.chatRoomCreatedEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { chatRoom ->
- sharedViewModel.selectedChatRoom.value = chatRoom
- navigateToChatRoom(AppUtils.createBundleWithSharedTextAndFiles(sharedViewModel))
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { chatRoom ->
+ sharedViewModel.selectedChatRoom.value = chatRoom
+ navigateToChatRoom(AppUtils.createBundleWithSharedTextAndFiles(sharedViewModel))
}
- )
+ }
viewModel.filter.observe(
- viewLifecycleOwner,
- {
- viewModel.applyFilter()
- }
- )
+ viewLifecycleOwner
+ ) {
+ viewModel.applyFilter()
+ }
adapter.selectedContact.observe(
- viewLifecycleOwner,
- {
- it.consume { searchResult ->
- if (createGroup) {
- viewModel.toggleSelectionForSearchResult(searchResult)
- } else {
- viewModel.createOneToOneChat(searchResult)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { searchResult ->
+ if (createGroup) {
+ viewModel.toggleSelectionForSearchResult(searchResult)
+ } else {
+ viewModel.createOneToOneChat(searchResult)
}
}
- )
+ }
addParticipantsFromSharedViewModel()
@@ -160,13 +153,12 @@ class ChatRoomCreationFragment : SecureFragment
}
viewModel.onErrorEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { messageResourceId ->
- (activity as MainActivity).showSnackBar(messageResourceId)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { messageResourceId ->
+ (activity as MainActivity).showSnackBar(messageResourceId)
}
- )
+ }
if (!PermissionHelper.get().hasReadContactsPermission()) {
Log.i("[Chat Room Creation] Asking for READ_CONTACTS permission")
diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt
index 273cf82f2..f8c2e012e 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt
@@ -29,19 +29,20 @@ import android.provider.MediaStore
import android.view.*
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.PopupWindow
+import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.view.doOnPreDraw
import androidx.databinding.DataBindingUtil
+import androidx.databinding.ViewDataBinding
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import java.io.File
import java.lang.IllegalArgumentException
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
+import kotlinx.coroutines.*
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
@@ -52,6 +53,7 @@ import org.linphone.activities.main.chat.adapters.ChatMessagesListAdapter
import org.linphone.activities.main.chat.data.ChatMessageData
import org.linphone.activities.main.chat.data.EventLogData
import org.linphone.activities.main.chat.viewmodels.*
+import org.linphone.activities.main.chat.views.RichEditTextSendListener
import org.linphone.activities.main.fragments.MasterFragment
import org.linphone.activities.main.viewmodels.DialogViewModel
import org.linphone.activities.main.viewmodels.SharedMainViewModel
@@ -107,6 +109,20 @@ class DetailChatRoomFragment : MasterFragment
- adapter.setUnreadMessageCount(viewModel.chatRoom.unreadMessagesCount, viewModel.isUserScrollingUp.value == true)
- adapter.submitList(events)
- }
- )
+ viewLifecycleOwner
+ ) { events ->
+ adapter.setUnreadMessageCount(
+ viewModel.chatRoom.unreadMessagesCount,
+ viewModel.isUserScrollingUp.value == true
+ )
+ adapter.submitList(events)
+ }
listViewModel.messageUpdatedEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { position ->
- adapter.notifyItemChanged(position)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { position ->
+ adapter.notifyItemChanged(position)
}
- )
+ }
listViewModel.requestWriteExternalStoragePermissionEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
}
- )
+ }
adapter.deleteMessageEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { chatMessage ->
- listViewModel.deleteMessage(chatMessage)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { chatMessage ->
+ listViewModel.deleteMessage(chatMessage)
+ viewModel.updateLastMessageToDisplay()
}
- )
+ }
adapter.resendMessageEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { chatMessage ->
- listViewModel.resendMessage(chatMessage)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { chatMessage ->
+ listViewModel.resendMessage(chatMessage)
}
- )
+ }
adapter.forwardMessageEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { chatMessage ->
- // Remove observer before setting the message to forward
- // as we don't want to forward it in this chat room
- sharedViewModel.messageToForwardEvent.removeObservers(viewLifecycleOwner)
- sharedViewModel.messageToForwardEvent.value = Event(chatMessage)
- sharedViewModel.isPendingMessageForward.value = true
+ viewLifecycleOwner
+ ) {
+ it.consume { chatMessage ->
+ // Remove observer before setting the message to forward
+ // as we don't want to forward it in this chat room
+ sharedViewModel.messageToForwardEvent.removeObservers(viewLifecycleOwner)
+ sharedViewModel.messageToForwardEvent.value = Event(chatMessage)
+ sharedViewModel.isPendingMessageForward.value = true
- if (sharedViewModel.isSlidingPaneSlideable.value == true) {
- Log.i("[Chat Room] Forwarding message, going to chat rooms list")
- sharedViewModel.closeSlidingPaneEvent.value = Event(true)
- } else {
- navigateToEmptyChatRoom()
- }
+ if (sharedViewModel.isSlidingPaneSlideable.value == true) {
+ Log.i("[Chat Room] Forwarding message, going to chat rooms list")
+ sharedViewModel.closeSlidingPaneEvent.value = Event(true)
+ } else {
+ navigateToEmptyChatRoom()
}
}
- )
+ }
adapter.replyMessageEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { chatMessage ->
- chatSendingViewModel.pendingChatMessageToReplyTo.value?.destroy()
- chatSendingViewModel.pendingChatMessageToReplyTo.value = ChatMessageData(chatMessage)
- chatSendingViewModel.isPendingAnswer.value = true
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { chatMessage ->
+ chatSendingViewModel.pendingChatMessageToReplyTo.value?.destroy()
+ chatSendingViewModel.pendingChatMessageToReplyTo.value =
+ ChatMessageData(chatMessage)
+ chatSendingViewModel.isPendingAnswer.value = true
}
- )
+ }
adapter.showImdnForMessageEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { chatMessage ->
- val args = Bundle()
- args.putString("MessageId", chatMessage.messageId)
- navigateToImdn(args)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { chatMessage ->
+ val args = Bundle()
+ args.putString("MessageId", chatMessage.messageId)
+ navigateToImdn(args)
}
- )
+ }
adapter.addSipUriToContactEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { sipUri ->
- Log.i("[Chat Room] Going to contacts list with SIP URI to add: $sipUri")
- sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event(R.id.masterChatRoomsFragment)
- navigateToContacts(sipUri)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { sipUri ->
+ Log.i("[Chat Room] Going to contacts list with SIP URI to add: $sipUri")
+ sharedViewModel.updateContactsAnimationsBasedOnDestination.value =
+ Event(R.id.masterChatRoomsFragment)
+ navigateToContacts(sipUri)
}
- )
+ }
adapter.openContentEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { content ->
- val path = content.filePath.orEmpty()
+ viewLifecycleOwner
+ ) {
+ it.consume { content ->
+ val path = content.filePath.orEmpty()
- if (!File(path).exists()) {
- (requireActivity() as MainActivity).showSnackBar(R.string.chat_room_file_not_found)
- } else {
- Log.i("[Chat Message] Opening file: $path")
- sharedViewModel.contentToOpen.value = content
+ if (!File(path).exists()) {
+ (requireActivity() as MainActivity).showSnackBar(R.string.chat_room_file_not_found)
+ } else {
+ Log.i("[Chat Message] Opening file: $path")
+ sharedViewModel.contentToOpen.value = content
- if (corePreferences.useInAppFileViewerForNonEncryptedFiles || content.isFileEncrypted) {
- val preventScreenshots =
- viewModel.chatRoom.currentParams.encryptionEnabled()
- when {
- FileUtils.isExtensionImage(path) -> navigateToImageFileViewer(
- preventScreenshots
- )
- FileUtils.isExtensionVideo(path) -> navigateToVideoFileViewer(
- preventScreenshots
- )
- FileUtils.isExtensionAudio(path) -> navigateToAudioFileViewer(
- preventScreenshots
- )
- FileUtils.isExtensionPdf(path) -> navigateToPdfFileViewer(
- preventScreenshots
- )
- FileUtils.isPlainTextFile(path) -> navigateToTextFileViewer(
- preventScreenshots
- )
- else -> {
- if (content.isFileEncrypted) {
- Log.w("[Chat Message] File is encrypted and can't be opened in one of our viewers...")
- showDialogForUserConsentBeforeExportingFileInThirdPartyApp(content)
- } else if (!FileUtils.openFileInThirdPartyApp(requireActivity(), path)) {
- showDialogToSuggestOpeningFileAsText()
- }
+ if (corePreferences.useInAppFileViewerForNonEncryptedFiles || content.isFileEncrypted) {
+ val preventScreenshots =
+ viewModel.chatRoom.currentParams.isEncryptionEnabled
+ when {
+ FileUtils.isExtensionImage(path) -> navigateToImageFileViewer(
+ preventScreenshots
+ )
+ FileUtils.isExtensionVideo(path) -> navigateToVideoFileViewer(
+ preventScreenshots
+ )
+ FileUtils.isExtensionAudio(path) -> navigateToAudioFileViewer(
+ preventScreenshots
+ )
+ FileUtils.isExtensionPdf(path) -> navigateToPdfFileViewer(
+ preventScreenshots
+ )
+ FileUtils.isPlainTextFile(path) -> navigateToTextFileViewer(
+ preventScreenshots
+ )
+ else -> {
+ if (content.isFileEncrypted) {
+ Log.w("[Chat Message] File is encrypted and can't be opened in one of our viewers...")
+ showDialogForUserConsentBeforeExportingFileInThirdPartyApp(
+ content
+ )
+ } else if (!FileUtils.openFileInThirdPartyApp(
+ requireActivity(),
+ path
+ )
+ ) {
+ showDialogToSuggestOpeningFileAsText()
}
}
- } else {
- if (!FileUtils.openFileInThirdPartyApp(requireActivity(), path)) {
- showDialogToSuggestOpeningFileAsText()
- }
+ }
+ } else {
+ if (!FileUtils.openFileInThirdPartyApp(requireActivity(), path)) {
+ showDialogToSuggestOpeningFileAsText()
}
}
}
}
- )
+ }
+
+ adapter.sipUriClickedEvent.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume { sipUri ->
+ val args = Bundle()
+ args.putString("URI", sipUri)
+ args.putBoolean("Transfer", false)
+ // If auto start call setting is enabled, ignore it
+ args.putBoolean("SkipAutoCallStart", true)
+ navigateToDialer(args)
+ }
+ }
adapter.scrollToChatMessageEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { chatMessage ->
- val events = listViewModel.events.value.orEmpty()
- val eventLog = events.find { eventLog ->
- if (eventLog.eventLog.type == EventLog.Type.ConferenceChatMessage) {
- (eventLog.data as ChatMessageData).chatMessage.messageId == chatMessage.messageId
- } else false
- }
- val index = events.indexOf(eventLog)
- try {
- if (corePreferences.enableAnimations) {
- binding.chatMessagesList.smoothScrollToPosition(index)
- } else {
- binding.chatMessagesList.scrollToPosition(index)
- }
- } catch (iae: IllegalArgumentException) {
- Log.e("[Chat Room] Can't scroll to position $index")
+ viewLifecycleOwner
+ ) {
+ it.consume { chatMessage ->
+ val events = listViewModel.events.value.orEmpty()
+ val eventLog = events.find { eventLog ->
+ if (eventLog.eventLog.type == EventLog.Type.ConferenceChatMessage) {
+ (eventLog.data as ChatMessageData).chatMessage.messageId == chatMessage.messageId
+ } else false
+ }
+ val index = events.indexOf(eventLog)
+ try {
+ if (corePreferences.enableAnimations) {
+ binding.chatMessagesList.smoothScrollToPosition(index)
+ } else {
+ binding.chatMessagesList.scrollToPosition(index)
}
+ } catch (iae: IllegalArgumentException) {
+ Log.e("[Chat Room] Can't scroll to position $index")
}
}
- )
+ }
binding.setBackClickListener {
goBack()
@@ -493,8 +554,20 @@ class DetailChatRoomFragment : MasterFragment
- Log.i("[Chat] Found rich content URI: $uri")
- lifecycleScope.launch {
- withContext(Dispatchers.Main) {
- val path = FileUtils.getFilePath(requireContext(), uri)
- Log.i("[Chat] Rich content URI: $uri matching path is: $path")
- if (path != null) {
- chatSendingViewModel.addAttachment(path)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { uri ->
+ Log.i("[Chat] Found rich content URI: $uri")
+ lifecycleScope.launch {
+ withContext(Dispatchers.Main) {
+ val path = FileUtils.getFilePath(requireContext(), uri)
+ Log.i("[Chat] Rich content URI: $uri matching path is: $path")
+ if (path != null) {
+ chatSendingViewModel.addAttachment(path)
}
}
}
}
- )
+ }
sharedViewModel.messageToForwardEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { chatMessage ->
- Log.i("[Chat Room] Found message to transfer")
- showForwardConfirmationDialog(chatMessage)
- sharedViewModel.isPendingMessageForward.value = false
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { chatMessage ->
+ Log.i("[Chat Room] Found message to transfer")
+ showForwardConfirmationDialog(chatMessage)
+ sharedViewModel.isPendingMessageForward.value = false
}
- )
+ }
+
+ binding.stubbedMessageToReplyTo.setOnInflateListener { _, inflated ->
+ Log.i("[Chat Room] Replying to message layout inflated")
+ val binding = DataBindingUtil.bind(inflated)
+ binding?.lifecycleOwner = viewLifecycleOwner
+ }
+
+ binding.stubbedVoiceRecording.setOnInflateListener { _, inflated ->
+ Log.i("[Chat Room] Voice recording layout inflated")
+ val binding = DataBindingUtil.bind(inflated)
+ binding?.lifecycleOwner = viewLifecycleOwner
+ }
}
override fun deleteItems(indexesOfItemToDelete: ArrayList) {
@@ -545,6 +628,7 @@ class DetailChatRoomFragment : MasterFragment 0) {
// Scroll to first unread message if any
@@ -858,11 +972,13 @@ class DetailChatRoomFragment : MasterFragment() {
return
}
- isSecure = chatRoom.currentParams.encryptionEnabled()
+ isSecure = chatRoom.currentParams.isEncryptionEnabled
listViewModel = ViewModelProvider(
this,
@@ -66,4 +66,10 @@ class DevicesFragment : SecureFragment() {
goBack()
}
}
+
+ override fun onResume() {
+ super.onResume()
+
+ listViewModel.updateParticipants()
+ }
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt
index 7777b379a..4baa685a3 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt
@@ -61,7 +61,7 @@ class GroupInfoFragment : SecureFragment() {
}
val chatRoom: ChatRoom? = sharedViewModel.selectedGroupChatRoom.value
- isSecure = chatRoom?.currentParams?.encryptionEnabled() ?: false
+ isSecure = chatRoom?.currentParams?.isEncryptionEnabled ?: false
viewModel = ViewModelProvider(
this,
@@ -84,36 +84,32 @@ class GroupInfoFragment : SecureFragment() {
binding.participants.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
viewModel.participants.observe(
- viewLifecycleOwner,
- {
- adapter.submitList(it)
- }
- )
+ viewLifecycleOwner
+ ) {
+ adapter.submitList(it)
+ }
viewModel.isMeAdmin.observe(
- viewLifecycleOwner,
- { isMeAdmin ->
- adapter.showAdminControls(isMeAdmin && chatRoom != null)
- }
- )
+ viewLifecycleOwner
+ ) { isMeAdmin ->
+ adapter.showAdminControls(isMeAdmin && chatRoom != null)
+ }
viewModel.meAdminChangedEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { isMeAdmin ->
- showMeAdminStateChanged(isMeAdmin)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { isMeAdmin ->
+ showMeAdminStateChanged(isMeAdmin)
}
- )
+ }
adapter.participantRemovedEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { participant ->
- viewModel.removeParticipant(participant)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { participant ->
+ viewModel.removeParticipant(participant)
}
- )
+ }
addParticipantsFromSharedViewModel()
@@ -122,22 +118,20 @@ class GroupInfoFragment : SecureFragment() {
}
viewModel.createdChatRoomEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { chatRoom ->
- goToChatRoom(chatRoom, true)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { chatRoom ->
+ goToChatRoom(chatRoom, true)
}
- )
+ }
viewModel.updatedChatRoomEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { chatRoom ->
- goToChatRoom(chatRoom, false)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { chatRoom ->
+ goToChatRoom(chatRoom, false)
}
- )
+ }
binding.setNextClickListener {
if (viewModel.chatRoom != null) {
@@ -182,13 +176,12 @@ class GroupInfoFragment : SecureFragment() {
}
viewModel.onErrorEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { messageResourceId ->
- (activity as MainActivity).showSnackBar(messageResourceId)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { messageResourceId ->
+ (activity as MainActivity).showSnackBar(messageResourceId)
}
- )
+ }
}
private fun addParticipantsFromSharedViewModel() {
diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt
index d59a75438..e50e5d0a1 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt
@@ -61,7 +61,7 @@ class ImdnFragment : SecureFragment() {
return
}
- isSecure = chatRoom.currentParams.encryptionEnabled()
+ isSecure = chatRoom.currentParams.isEncryptionEnabled
if (arguments != null) {
val messageId = arguments?.getString("MessageId")
@@ -98,11 +98,10 @@ class ImdnFragment : SecureFragment() {
binding.participantsList.addItemDecoration(headerItemDecoration)
viewModel.participants.observe(
- viewLifecycleOwner,
- {
- adapter.submitList(it)
- }
- )
+ viewLifecycleOwner
+ ) {
+ adapter.submitList(it)
+ }
binding.setBackClickListener {
goBack()
diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt
index 54968d878..eff7aedc1 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt
@@ -98,7 +98,9 @@ class MasterChatRoomsFragment : MasterFragment
- adapter.submitList(chatRooms)
- }
- )
+ viewLifecycleOwner
+ ) { chatRooms ->
+ adapter.submitList(chatRooms)
+ }
listViewModel.contactsUpdatedEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- adapter.notifyDataSetChanged()
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ adapter.notifyDataSetChanged()
}
- )
+ }
adapter.selectedChatRoomEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { chatRoom ->
- if ((requireActivity() as GenericActivity).isDestructionPending) {
- Log.w("[Chat] Activity is pending destruction, don't start navigating now!")
- sharedViewModel.destructionPendingChatRoom = chatRoom
- } else {
- if (chatRoom.peerAddress.asStringUriOnly() == coreContext.notificationsManager.currentlyDisplayedChatRoomAddress) {
- Log.w("[Chat] This chat room is already displayed!")
+ viewLifecycleOwner
+ ) {
+ it.consume { chatRoom ->
+ if ((requireActivity() as GenericActivity).isDestructionPending) {
+ Log.w("[Chat] Activity is pending destruction, don't start navigating now!")
+ sharedViewModel.destructionPendingChatRoom = chatRoom
+ } else {
+ if (chatRoom.peerAddress.asStringUriOnly() == coreContext.notificationsManager.currentlyDisplayedChatRoomAddress) {
+ if (!binding.slidingPane.isOpen) {
+ Log.w("[Chat] Chat room is displayed but sliding pane is closed...")
+ if (!binding.slidingPane.openPane()) {
+ Log.e("[Chat] Tried to open pane to workaround already displayed chat room issue, failed!")
+ }
} else {
- sharedViewModel.selectedChatRoom.value = chatRoom
- navigateToChatRoom(
- AppUtils.createBundleWithSharedTextAndFiles(
- sharedViewModel
- )
- )
+ Log.w("[Chat] This chat room is already displayed!")
}
+ } else {
+ sharedViewModel.selectedChatRoom.value = chatRoom
+ navigateToChatRoom(
+ AppUtils.createBundleWithSharedTextAndFiles(
+ sharedViewModel
+ )
+ )
}
}
}
- )
+ }
binding.setEditClickListener {
listSelectionViewModel.isEditionEnabled.value = true
@@ -315,56 +308,52 @@ class MasterChatRoomsFragment : MasterFragment
- (activity as MainActivity).showSnackBar(messageResourceId)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { messageResourceId ->
+ (activity as MainActivity).showSnackBar(messageResourceId)
}
- )
+ }
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt
index f7e0de69c..37290f8f3 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt
@@ -19,6 +19,7 @@
*/
package org.linphone.activities.main.chat.viewmodels
+import android.view.inputmethod.EditorInfo
import android.widget.Toast
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
@@ -37,6 +38,7 @@ import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.activities.main.chat.data.ChatMessageAttachmentData
import org.linphone.activities.main.chat.data.ChatMessageData
+import org.linphone.compatibility.Compatibility
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
@@ -86,11 +88,18 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
val isPlayingVoiceRecording = MutableLiveData()
- val recorder: Recorder
-
val voiceRecordPlayingPosition = MutableLiveData()
- var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
+ val imeFlags: Int = if (chatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) {
+ // IME_FLAG_NO_PERSONALIZED_LEARNING is only available on Android 8 and newer
+ Compatibility.getImeFlagsForSecureChatRoom()
+ } else {
+ EditorInfo.IME_FLAG_NO_EXTRACT_UI
+ }
+
+ private val recorder: Recorder
+
+ private var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
private lateinit var voiceRecordingPlayer: Player
private val playerListener = PlayerListener {
@@ -98,9 +107,19 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
stopVoiceRecordPlayer()
}
+ private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() {
+ override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
+ if (state == ChatRoom.State.Created || state == ChatRoom.State.Terminated) {
+ isReadOnly.value = chatRoom.hasBeenLeft()
+ }
+ }
+ }
+
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
init {
+ chatRoom.addListener(chatRoomListener)
+
attachments.value = arrayListOf()
attachFileEnabled.value = true
@@ -118,6 +137,7 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
override fun onCleared() {
attachments.value.orEmpty().forEach(ChatMessageAttachmentData::destroy)
+ pendingChatMessageToReplyTo.value?.destroy()
if (recorder.state != RecorderState.Closed) {
recorder.close()
@@ -128,6 +148,7 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
voiceRecordingPlayer.removeListener(playerListener)
}
+ chatRoom.removeListener(chatRoomListener)
scope.cancel()
super.onCleared()
}
@@ -418,8 +439,9 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
private fun initVoiceRecordPlayer() {
Log.i("[Chat Message Sending] Creating player for voice record")
- // Use speaker sound card to play recordings, otherwise use earpiece
+ // In case no headphones/headset is connected, use speaker sound card to play recordings, otherwise use earpiece
// If none are available, default one will be used
+ var headphonesCard: String? = null
var speakerCard: String? = null
var earpieceCard: String? = null
for (device in coreContext.core.audioDevices) {
@@ -428,12 +450,14 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
speakerCard = device.id
} else if (device.type == AudioDevice.Type.Earpiece) {
earpieceCard = device.id
+ } else if (device.type == AudioDevice.Type.Headphones || device.type == AudioDevice.Type.Headset) {
+ headphonesCard = device.id
}
}
}
- Log.i("[Chat Message Sending] Found speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]")
+ Log.i("[Chat Message Sending] Found headset/headphones sound card [$headphonesCard], speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]")
- val localPlayer = coreContext.core.createLocalPlayer(speakerCard ?: earpieceCard, null, null)
+ val localPlayer = coreContext.core.createLocalPlayer(headphonesCard ?: speakerCard ?: earpieceCard, null, null)
if (localPlayer != null) {
voiceRecordingPlayer = localPlayer
} else {
diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessagesListViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessagesListViewModel.kt
index 61a74bad6..8821cd8f1 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessagesListViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessagesListViewModel.kt
@@ -158,27 +158,21 @@ class ChatMessagesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
}
fun deleteMessage(chatMessage: ChatMessage) {
- val position: Int = chatMessage.userData as Int
LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage)
chatRoom.deleteMessage(chatMessage)
- val list = arrayListOf()
- list.addAll(events.value.orEmpty())
- list.removeAt(position)
- events.value = list
+ events.value.orEmpty().forEach(EventLogData::destroy)
+ events.value = getEvents()
}
fun deleteEventLogs(listToDelete: ArrayList) {
- val list = arrayListOf()
- list.addAll(events.value.orEmpty())
-
for (eventLog in listToDelete) {
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog.eventLog)
eventLog.eventLog.deleteFromDatabase()
- list.remove(eventLog)
}
- events.value = list
+ events.value.orEmpty().forEach(EventLogData::destroy)
+ events.value = getEvents()
}
fun loadMoreData(totalItemsCount: Int) {
@@ -248,6 +242,8 @@ class ChatMessagesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage)
chatRoom.deleteMessage(chatMessage)
}
+
+ events.value.orEmpty().forEach(EventLogData::destroy)
events.value = getEvents()
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt
index 17cefca50..bb1cc356e 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt
@@ -160,9 +160,9 @@ class ChatRoomCreationViewModel : ErrorReportingViewModel() {
val encrypted = isEncrypted.value == true
val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams()
params.backend = ChatRoomBackend.Basic
- params.enableGroup(false)
+ params.isGroupEnabled = false
if (encrypted) {
- params.enableEncryption(true)
+ params.isEncryptionEnabled = true
params.backend = ChatRoomBackend.FlexisipChat
params.ephemeralMode = if (corePreferences.useEphemeralPerDeviceMode)
ChatRoomEphemeralMode.DeviceManaged
diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt
index e91decdc7..b84b82090 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt
@@ -74,6 +74,8 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
val peerSipUri = MutableLiveData()
+ val ephemeralEnabled = MutableLiveData()
+
val oneToOneChatRoom: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())
val encryptedChatRoom: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())
@@ -88,12 +90,12 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
var oneParticipantOneDevice: Boolean = false
- var addressToCall: Address? = null
-
var onlyParticipantOnlyDeviceAddress: Address? = null
val chatUnreadCountTranslateY = MutableLiveData()
+ private var addressToCall: Address? = null
+
private val bounceAnimator: ValueAnimator by lazy {
ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.tabs_fragment_unread_count_bounce_offset), 0f).apply {
addUpdateListener {
@@ -111,6 +113,7 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
override fun onContactsUpdated() {
Log.i("[Chat Room] Contacts have changed")
contactLookup()
+ updateLastMessageToDisplay()
}
}
@@ -196,7 +199,11 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) {
Log.i("[Chat Room] Ephemeral message deleted, updated last message displayed")
- lastMessageText.value = formatLastMessage(chatRoom.lastMessageInHistory)
+ updateLastMessageToDisplay()
+ }
+
+ override fun onEphemeralEvent(chatRoom: ChatRoom, eventLog: EventLog) {
+ ephemeralEnabled.value = chatRoom.isEphemeralEnabled
}
override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) {
@@ -209,16 +216,17 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
chatRoom.addListener(chatRoomListener)
coreContext.contactsManager.addListener(contactsUpdatedListener)
- lastMessageText.value = formatLastMessage(chatRoom.lastMessageInHistory)
unreadMessagesCount.value = chatRoom.unreadMessagesCount
lastUpdate.value = TimestampUtils.toString(chatRoom.lastUpdateTime, true)
subject.value = chatRoom.subject
updateSecurityIcon()
meAdmin.value = chatRoom.me?.isAdmin ?: false
+ ephemeralEnabled.value = chatRoom.isEphemeralEnabled
contactLookup()
updateParticipants()
+ updateLastMessageToDisplay()
callInProgress.value = chatRoom.core.callsNb > 0
updateRemotesComposing()
@@ -268,6 +276,10 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
}
}
+ fun updateLastMessageToDisplay() {
+ lastMessageText.value = formatLastMessage(chatRoom.lastMessageInHistory)
+ }
+
private fun formatLastMessage(msg: ChatMessage?): String {
if (msg == null) return ""
diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt
index 14f943db2..1e9982c5f 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt
@@ -142,9 +142,7 @@ class ChatRoomsListViewModel : ErrorReportingViewModel() {
}
private fun updateChatRooms() {
- for (chatRoomViewModel in chatRooms.value.orEmpty()) {
- chatRoomViewModel.destroy()
- }
+ chatRooms.value.orEmpty().forEach(ChatRoomViewModel::destroy)
val list = arrayListOf()
for (chatRoom in coreContext.core.chatRooms) {
@@ -155,6 +153,14 @@ class ChatRoomsListViewModel : ErrorReportingViewModel() {
}
private fun addChatRoom(chatRoom: ChatRoom) {
+ val exists = chatRooms.value.orEmpty().find {
+ it.chatRoom.localAddress.weakEqual(chatRoom.localAddress) && it.chatRoom.peerAddress.weakEqual(chatRoom.peerAddress)
+ }
+ if (exists != null) {
+ Log.w("[Chat Rooms] Do not add chat room to list, it's already here")
+ return
+ }
+
val list = arrayListOf()
val viewModel = ChatRoomViewModel(chatRoom)
list.add(viewModel)
@@ -170,12 +176,10 @@ class ChatRoomsListViewModel : ErrorReportingViewModel() {
}
private fun findChatRoomIndex(chatRoom: ChatRoom): Int {
- var index = 0
- for (chatRoomViewModel in chatRooms.value.orEmpty()) {
+ for ((index, chatRoomViewModel) in chatRooms.value.orEmpty().withIndex()) {
if (chatRoomViewModel.chatRoom == chatRoom) {
return index
}
- index++
}
return -1
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListViewModel.kt
index 3f61d5497..625ed9419 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListViewModel.kt
@@ -59,16 +59,16 @@ class DevicesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
init {
chatRoom.addListener(listener)
- updateParticipants()
}
override fun onCleared() {
participants.value.orEmpty().forEach(DevicesListGroupData::destroy)
chatRoom.removeListener(listener)
+
super.onCleared()
}
- private fun updateParticipants() {
+ fun updateParticipants() {
participants.value.orEmpty().forEach(DevicesListGroupData::destroy)
val list = arrayListOf()
diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/EphemeralViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/EphemeralViewModel.kt
index 3704ef1ae..ce56f8034 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/EphemeralViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/EphemeralViewModel.kt
@@ -50,8 +50,8 @@ class EphemeralViewModel(private val chatRoom: ChatRoom) : ViewModel() {
}
init {
- Log.i("[Ephemeral Messages] Current lifetime is ${chatRoom.ephemeralLifetime}, ephemeral enabled? ${chatRoom.ephemeralEnabled()}")
- currentSelectedDuration = if (chatRoom.ephemeralEnabled()) chatRoom.ephemeralLifetime else 0
+ Log.i("[Ephemeral Messages] Current lifetime is ${chatRoom.ephemeralLifetime}, ephemeral enabled? ${chatRoom.isEphemeralEnabled}")
+ currentSelectedDuration = if (chatRoom.isEphemeralEnabled) chatRoom.ephemeralLifetime else 0
computeEphemeralDurationValues()
}
@@ -65,13 +65,13 @@ class EphemeralViewModel(private val chatRoom: ChatRoom) : ViewModel() {
Log.i("[Ephemeral Messages] Configured lifetime for ephemeral messages was already $currentSelectedDuration")
}
- if (!chatRoom.ephemeralEnabled()) {
+ if (!chatRoom.isEphemeralEnabled) {
Log.i("[Ephemeral Messages] Ephemeral messages were disabled, enable them")
- chatRoom.enableEphemeral(true)
+ chatRoom.isEphemeralEnabled = true
}
- } else if (chatRoom.ephemeralEnabled()) {
+ } else if (chatRoom.isEphemeralEnabled) {
Log.i("[Ephemeral Messages] Ephemeral messages were enabled, disable them")
- chatRoom.enableEphemeral(false)
+ chatRoom.isEphemeralEnabled = false
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt
index aa5795d7a..7bfd94e48 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt
@@ -117,8 +117,8 @@ class GroupInfoViewModel(val chatRoom: ChatRoom?) : ErrorReportingViewModel() {
fun createChatRoom() {
waitForChatRoomCreation.value = true
val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams()
- params.enableEncryption(isEncrypted.value == true)
- params.enableGroup(true)
+ params.isEncryptionEnabled = isEncrypted.value == true
+ params.isGroupEnabled = true
if (isEncrypted.value == true) {
params.ephemeralMode = if (corePreferences.useEphemeralPerDeviceMode)
ChatRoomEphemeralMode.DeviceManaged
diff --git a/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt b/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt
index 059f24c9c..f06828e54 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt
@@ -21,6 +21,7 @@ package org.linphone.activities.main.chat.views
import android.content.Context
import android.text.Layout
+import android.text.method.LinkMovementMethod
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import kotlin.math.ceil
@@ -40,6 +41,12 @@ class MultiLineWrapContentWidthTextView : AppCompatTextView {
defStyleAttr: Int
) : super(context, attrs, defStyleAttr)
+ override fun setText(text: CharSequence?, type: BufferType?) {
+ super.setText(text, type)
+ // Required for PatternClickableSpan
+ movementMethod = LinkMovementMethod.getInstance()
+ }
+
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
var wSpec = widthSpec
val widthMode = MeasureSpec.getMode(wSpec)
diff --git a/app/src/main/java/org/linphone/activities/main/chat/views/RichEditText.kt b/app/src/main/java/org/linphone/activities/main/chat/views/RichEditText.kt
index f6978ce32..aa44cfc5c 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/views/RichEditText.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/views/RichEditText.kt
@@ -22,6 +22,7 @@ package org.linphone.activities.main.chat.views
import android.app.Activity
import android.content.Context
import android.util.AttributeSet
+import android.view.KeyEvent
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.view.ViewCompat
import androidx.lifecycle.ViewModelProvider
@@ -35,6 +36,10 @@ import org.linphone.utils.Event
* Allows for image input inside an EditText, usefull for keyboards with gif support for example.
*/
class RichEditText : AppCompatEditText {
+ private var controlPressed = false
+
+ private var sendListener: RichEditTextSendListener? = null
+
constructor(context: Context) : super(context) {
initReceiveContentListener()
}
@@ -51,6 +56,10 @@ class RichEditText : AppCompatEditText {
initReceiveContentListener()
}
+ fun setControlEnterListener(listener: RichEditTextSendListener) {
+ sendListener = listener
+ }
+
private fun initReceiveContentListener() {
ViewCompat.setOnReceiveContentListener(
this, RichContentReceiver.MIME_TYPES,
@@ -63,5 +72,25 @@ class RichEditText : AppCompatEditText {
sharedViewModel.richContentUri.value = Event(uri)
}
)
+
+ setOnKeyListener { _, keyCode, event ->
+ if (keyCode == KeyEvent.KEYCODE_CTRL_LEFT) {
+ if (event.action == KeyEvent.ACTION_DOWN) {
+ controlPressed = true
+ } else if (event.action == KeyEvent.ACTION_UP) {
+ controlPressed = false
+ }
+ false
+ } else if (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_UP && controlPressed) {
+ sendListener?.onControlEnterPressedAndReleased()
+ true
+ } else {
+ false
+ }
+ }
}
}
+
+interface RichEditTextSendListener {
+ fun onControlEnterPressedAndReleased()
+}
diff --git a/app/src/main/java/org/linphone/activities/main/contact/adapters/ContactsListAdapter.kt b/app/src/main/java/org/linphone/activities/main/contact/adapters/ContactsListAdapter.kt
index ee2e554cb..3772c35a4 100644
--- a/app/src/main/java/org/linphone/activities/main/contact/adapters/ContactsListAdapter.kt
+++ b/app/src/main/java/org/linphone/activities/main/contact/adapters/ContactsListAdapter.kt
@@ -71,15 +71,14 @@ class ContactsListAdapter(
// This is for item selection through ListTopBarFragment
selectionListViewModel = selectionViewModel
selectionViewModel.isEditionEnabled.observe(
- viewLifecycleOwner,
- {
- position = adapterPosition
- }
- )
+ viewLifecycleOwner
+ ) {
+ position = bindingAdapterPosition
+ }
setClickListener {
if (selectionViewModel.isEditionEnabled.value == true) {
- selectionViewModel.onToggleSelect(adapterPosition)
+ selectionViewModel.onToggleSelect(bindingAdapterPosition)
} else {
selectedContactEvent.value = Event(contactViewModel.contactInternal)
}
diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt
index 5c39ad78e..cf7384fae 100644
--- a/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt
@@ -83,47 +83,50 @@ class DetailContactFragment : GenericFragment() {
binding.viewModel = viewModel
viewModel.sendSmsToEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { number ->
- sendSms(number)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { number ->
+ sendSms(number)
}
- )
+ }
viewModel.startCallToEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { address ->
- if (coreContext.core.callsNb > 0) {
- Log.i("[Contact] Starting dialer with pre-filled URI ${address.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}")
- sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event(R.id.dialerFragment)
- sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.masterContactsFragment)
+ viewLifecycleOwner
+ ) {
+ it.consume { address ->
+ if (coreContext.core.callsNb > 0) {
+ Log.i("[Contact] Starting dialer with pre-filled URI ${address.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}")
+ sharedViewModel.updateContactsAnimationsBasedOnDestination.value =
+ Event(R.id.dialerFragment)
+ sharedViewModel.updateDialerAnimationsBasedOnDestination.value =
+ Event(R.id.masterContactsFragment)
- val args = Bundle()
- args.putString("URI", address.asStringUriOnly())
- args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer)
- args.putBoolean("SkipAutoCallStart", true) // If auto start call setting is enabled, ignore it
- navigateToDialer(args)
- } else {
- coreContext.startCall(address)
- }
+ val args = Bundle()
+ args.putString("URI", address.asStringUriOnly())
+ args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer)
+ args.putBoolean(
+ "SkipAutoCallStart",
+ true
+ ) // If auto start call setting is enabled, ignore it
+ navigateToDialer(args)
+ } else {
+ coreContext.startCall(address)
}
}
- )
+ }
viewModel.chatRoomCreatedEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { chatRoom ->
- sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event(R.id.masterChatRoomsFragment)
- val args = Bundle()
- args.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly())
- args.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly())
- navigateToChatRoom(args)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { chatRoom ->
+ sharedViewModel.updateContactsAnimationsBasedOnDestination.value =
+ Event(R.id.masterChatRoomsFragment)
+ val args = Bundle()
+ args.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly())
+ args.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly())
+ navigateToChatRoom(args)
}
- )
+ }
binding.setBackClickListener {
goBack()
@@ -138,13 +141,12 @@ class DetailContactFragment : GenericFragment() {
}
viewModel.onErrorEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { messageResourceId ->
- (activity as MainActivity).showSnackBar(messageResourceId)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { messageResourceId ->
+ (activity as MainActivity).showSnackBar(messageResourceId)
}
- )
+ }
view.doOnPreDraw {
// Notifies fragment is ready to be drawn
diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt
index ff87f2483..bc419400f 100644
--- a/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt
@@ -85,75 +85,59 @@ class MasterContactsFragment : MasterFragment
- val forward = when (id) {
- R.id.dialerFragment, R.id.masterChatRoomsFragment -> false
- else -> true
- }
- if (corePreferences.enableAnimations) {
- val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
- val axis = if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y
- enterTransition = MaterialSharedAxis(axis, forward)
- reenterTransition = MaterialSharedAxis(axis, forward)
- returnTransition = MaterialSharedAxis(axis, !forward)
- exitTransition = MaterialSharedAxis(axis, !forward)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { id ->
+ val forward = when (id) {
+ R.id.dialerFragment, R.id.masterChatRoomsFragment -> false
+ else -> true
+ }
+ if (corePreferences.enableAnimations) {
+ val portraitOrientation =
+ resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
+ val axis =
+ if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y
+ enterTransition = MaterialSharedAxis(axis, forward)
+ reenterTransition = MaterialSharedAxis(axis, forward)
+ returnTransition = MaterialSharedAxis(axis, !forward)
+ exitTransition = MaterialSharedAxis(axis, !forward)
}
}
- )
+ }
sharedViewModel.contactFragmentOpenedEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- binding.slidingPane.openPane()
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ binding.slidingPane.openPane()
}
- )
+ }
sharedViewModel.closeSlidingPaneEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- if (!binding.slidingPane.closePane()) {
- goBack()
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ if (!binding.slidingPane.closePane()) {
+ goBack()
}
}
- )
+ }
sharedViewModel.layoutChangedEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
- if (binding.slidingPane.isSlideable) {
- val navHostFragment = childFragmentManager.findFragmentById(R.id.contacts_nav_container) as NavHostFragment
- if (navHostFragment.navController.currentDestination?.id == R.id.emptyContactFragment) {
- Log.i("[Contacts] Foldable device has been folded, closing side pane with empty fragment")
- binding.slidingPane.closePane()
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
+ if (binding.slidingPane.isSlideable) {
+ val navHostFragment =
+ childFragmentManager.findFragmentById(R.id.contacts_nav_container) as NavHostFragment
+ if (navHostFragment.navController.currentDestination?.id == R.id.emptyContactFragment) {
+ Log.i("[Contacts] Foldable device has been folded, closing side pane with empty fragment")
+ binding.slidingPane.closePane()
}
}
}
- )
+ }
binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
- /*binding.slidingPane.addPanelSlideListener(object : SlidingPaneLayout.PanelSlideListener {
- override fun onPanelSlide(panel: View, slideOffset: Float) { }
-
- override fun onPanelOpened(panel: View) {
- if (binding.slidingPane.isSlideable) {
- (requireActivity() as MainActivity).hideTabsFragment()
- }
- }
-
- override fun onPanelClosed(panel: View) {
- if (binding.slidingPane.isSlideable) {
- (requireActivity() as MainActivity).showTabsFragment()
- }
- }
- })*/
/* End of shared view model & sliding pane related */
@@ -190,13 +174,13 @@ class MasterContactsFragment : MasterFragment
- Log.i("[Contacts] Selected item in list changed: $contact")
- sharedViewModel.selectedContact.value = contact
+ viewLifecycleOwner
+ ) {
+ it.consume { contact ->
+ Log.i("[Contacts] Selected item in list changed: $contact")
+ sharedViewModel.selectedContact.value = contact
- if (editOnClick) {
- navigateToContactEditor(sipUriToAdd, binding.slidingPane)
- editOnClick = false
- sipUriToAdd = null
- } else {
- navigateToContact()
- }
+ if (editOnClick) {
+ navigateToContactEditor(sipUriToAdd, binding.slidingPane)
+ editOnClick = false
+ sipUriToAdd = null
+ } else {
+ navigateToContact()
}
}
- )
+ }
listViewModel.contactsList.observe(
- viewLifecycleOwner,
- {
- val id = contactIdToDisplay
- if (id != null) {
- val contact = coreContext.contactsManager.findContactById(id)
- if (contact != null) {
- contactIdToDisplay = null
- Log.i("[Contacts] Found matching contact $contact after callback")
- adapter.selectedContactEvent.value = Event(contact)
- }
+ viewLifecycleOwner
+ ) {
+ val id = contactIdToDisplay
+ if (id != null) {
+ val contact = coreContext.contactsManager.findContactById(id)
+ if (contact != null) {
+ contactIdToDisplay = null
+ Log.i("[Contacts] Found matching contact $contact after callback")
+ adapter.selectedContactEvent.value = Event(contact)
}
- adapter.submitList(it)
}
- )
+ adapter.submitList(it)
+ }
binding.setAllContactsToggleClickListener {
listViewModel.sipContactsSelected.value = false
@@ -264,18 +246,16 @@ class MasterContactsFragment : MasterFragment() {
useMaterialSharedAxisXForwardAnimation = false
sharedViewModel.updateDialerAnimationsBasedOnDestination.observe(
- viewLifecycleOwner,
- {
- it.consume { id ->
- val forward = when (id) {
- R.id.masterChatRoomsFragment -> false
- else -> true
- }
- if (corePreferences.enableAnimations) {
- val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
- val axis = if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y
- enterTransition = MaterialSharedAxis(axis, forward)
- reenterTransition = MaterialSharedAxis(axis, forward)
- returnTransition = MaterialSharedAxis(axis, !forward)
- exitTransition = MaterialSharedAxis(axis, !forward)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { id ->
+ val forward = when (id) {
+ R.id.masterChatRoomsFragment -> false
+ else -> true
+ }
+ if (corePreferences.enableAnimations) {
+ val portraitOrientation =
+ resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
+ val axis =
+ if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y
+ enterTransition = MaterialSharedAxis(axis, forward)
+ reenterTransition = MaterialSharedAxis(axis, forward)
+ returnTransition = MaterialSharedAxis(axis, !forward)
+ exitTransition = MaterialSharedAxis(axis, !forward)
}
}
- )
+ }
binding.setNewContactClickListener {
sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.masterContactsFragment)
@@ -108,6 +111,47 @@ class DialerFragment : SecureFragment() {
}
}
+ viewModel.enteredUri.observe(
+ viewLifecycleOwner
+ ) {
+ if (it == corePreferences.debugPopupCode) {
+ displayDebugPopup()
+ viewModel.enteredUri.value = ""
+ }
+ }
+
+ viewModel.uploadFinishedEvent.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume { url ->
+ // To prevent being trigger when using the Send Logs button in About page
+ if (uploadLogsInitiatedByUs) {
+ val clipboard =
+ requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText("Logs url", url)
+ clipboard.setPrimaryClip(clip)
+
+ val activity = requireActivity() as MainActivity
+ activity.showSnackBar(R.string.logs_url_copied_to_clipboard)
+
+ AppUtils.shareUploadedLogsUrl(activity, url)
+ }
+ }
+ }
+
+ viewModel.updateAvailableEvent.observe(
+ viewLifecycleOwner
+ ) {
+ it.consume { url ->
+ displayNewVersionAvailableDialog(url)
+ }
+ }
+
+ if (corePreferences.firstStart) {
+ Log.w("[Dialer] First start detected, wait for assistant to be finished to check for update & request permissions")
+ return
+ }
+
if (arguments?.containsKey("Transfer") == true) {
sharedViewModel.pendingCallTransfer = arguments?.getBoolean("Transfer") ?: false
Log.i("[Dialer] Is pending call transfer: ${sharedViewModel.pendingCallTransfer}")
@@ -127,45 +171,6 @@ class DialerFragment : SecureFragment() {
}
arguments?.clear()
- viewModel.enteredUri.observe(
- viewLifecycleOwner,
- {
- if (it == corePreferences.debugPopupCode) {
- displayDebugPopup()
- viewModel.enteredUri.value = ""
- }
- }
- )
-
- viewModel.uploadFinishedEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { url ->
- // To prevent being trigger when using the Send Logs button in About page
- if (uploadLogsInitiatedByUs) {
- val clipboard =
- requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
- val clip = ClipData.newPlainText("Logs url", url)
- clipboard.setPrimaryClip(clip)
-
- val activity = requireActivity() as MainActivity
- activity.showSnackBar(R.string.logs_url_copied_to_clipboard)
-
- AppUtils.shareUploadedLogsUrl(activity, url)
- }
- }
- }
- )
-
- viewModel.updateAvailableEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { url ->
- displayNewVersionAvailableDialog(url)
- }
- }
- )
-
Log.i("[Dialer] Pending call transfer mode = ${sharedViewModel.pendingCallTransfer}")
viewModel.transferVisibility.value = sharedViewModel.pendingCallTransfer
@@ -205,18 +210,72 @@ class DialerFragment : SecureFragment() {
Log.i("[Dialer] READ_PHONE_STATE permission has been granted")
coreContext.initPhoneStateListener()
}
+ checkTelecomManagerPermissions()
+ } else if (requestCode == 1) {
+ var allGranted = true
+ for (result in grantResults) {
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ allGranted = false
+ }
+ }
+ if (allGranted) {
+ Log.i("[Dialer] Telecom Manager permission have been granted")
+ enableTelecomManager()
+ } else {
+ Log.w("[Dialer] Telecom Manager permission have been denied (at least one of them)")
+ }
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
@TargetApi(Version.API23_MARSHMALLOW_60)
private fun checkPermissions() {
+ checkReadPhoneStatePermission()
+ if (Version.sdkAboveOrEqual(Version.API26_O_80) && PermissionHelper.get().hasReadPhoneStatePermission()) {
+ // Don't check the following the previous permission is being asked
+ checkTelecomManagerPermissions()
+ }
+ }
+
+ @TargetApi(Version.API23_MARSHMALLOW_60)
+ private fun checkReadPhoneStatePermission() {
if (!PermissionHelper.get().hasReadPhoneStatePermission()) {
Log.i("[Dialer] Asking for READ_PHONE_STATE permission")
requestPermissions(arrayOf(Manifest.permission.READ_PHONE_STATE), 0)
}
}
+ @TargetApi(Version.API26_O_80)
+ private fun checkTelecomManagerPermissions() {
+ if (!corePreferences.useTelecomManager) {
+ Log.i("[Dialer] Telecom Manager feature is disabled")
+ if (corePreferences.manuallyDisabledTelecomManager) {
+ Log.w("[Dialer] User has manually disabled Telecom Manager feature")
+ } else {
+ if (Compatibility.hasTelecomManagerPermissions(requireContext())) {
+ enableTelecomManager()
+ } else {
+ Log.i("[Dialer] Asking for Telecom Manager permissions")
+ Compatibility.requestTelecomManagerPermissions(requireActivity(), 1)
+ }
+ }
+ } else {
+ Log.i("[Dialer] Telecom Manager feature is already enabled")
+ }
+ }
+
+ @TargetApi(Version.API26_O_80)
+ private fun enableTelecomManager() {
+ Log.i("[Dialer] Telecom Manager permissions granted")
+ if (!TelecomHelper.exists()) {
+ Log.i("[Dialer] Creating Telecom Helper")
+ TelecomHelper.create(requireContext())
+ } else {
+ Log.e("[Dialer] Telecom Manager was already created ?!")
+ }
+ corePreferences.useTelecomManager = true
+ }
+
private fun displayDebugPopup() {
val alertDialog = MaterialAlertDialogBuilder(requireContext())
alertDialog.setTitle(getString(R.string.debug_popup_title))
diff --git a/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt b/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt
index 04f9bdebe..964399d6b 100644
--- a/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt
@@ -21,7 +21,6 @@ package org.linphone.activities.main.dialer.viewmodels
import android.content.Context
import android.os.Vibrator
-import android.provider.Settings
import android.text.Editable
import android.widget.EditText
import androidx.lifecycle.MutableLiveData
@@ -69,23 +68,8 @@ class DialerViewModel : LogsUploadViewModel() {
}
enteredUri.value = sb.toString()
- if (coreContext.core.callsNb == 0) {
- val contentResolver = coreContext.context.contentResolver
- try {
- if (Settings.System.getInt(
- contentResolver,
- Settings.System.DTMF_TONE_WHEN_DIALING
- ) == 1
- ) {
- coreContext.core.playDtmf(key, 1)
-
- if (vibrator.hasVibrator() && corePreferences.dtmfKeypadVibration) {
- Compatibility.eventVibration(vibrator)
- }
- }
- } catch (snfe: Settings.SettingNotFoundException) {
- Log.e("[Dialer] Can't play DTMF: $snfe")
- }
+ if (vibrator.hasVibrator() && corePreferences.dtmfKeypadVibration) {
+ Compatibility.eventVibration(vibrator)
}
}
@@ -176,7 +160,7 @@ class DialerViewModel : LogsUploadViewModel() {
fun updateShowVideoPreview() {
val videoPreview = corePreferences.videoPreview
showPreview.value = videoPreview
- coreContext.core.enableVideoPreview(videoPreview)
+ coreContext.core.isVideoPreviewEnabled = videoPreview
}
fun eraseLastChar() {
diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/GenericViewerFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/GenericViewerFragment.kt
index 661dd21cc..7367813fa 100644
--- a/app/src/main/java/org/linphone/activities/main/files/fragments/GenericViewerFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/files/fragments/GenericViewerFragment.kt
@@ -56,4 +56,8 @@ abstract class GenericViewerFragment : SecureFragment()
(childFragmentManager.findFragmentById(R.id.top_bar_fragment) as? TopBarFragment)
?.setContent(content)
}
+
+ override fun goBack() {
+ findNavController().popBackStack()
+ }
}
diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/TopBarFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/TopBarFragment.kt
index 0699df0a1..f8987756d 100644
--- a/app/src/main/java/org/linphone/activities/main/files/fragments/TopBarFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/files/fragments/TopBarFragment.kt
@@ -21,13 +21,21 @@ package org.linphone.activities.main.files.fragments
import android.os.Bundle
import android.view.View
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.linphone.R
import org.linphone.activities.GenericFragment
import org.linphone.activities.SnackBarActivity
+import org.linphone.compatibility.Compatibility
import org.linphone.core.Content
import org.linphone.core.tools.Log
import org.linphone.databinding.FileViewerTopBarFragmentBinding
+import org.linphone.mediastream.Version
import org.linphone.utils.FileUtils
+import org.linphone.utils.PermissionHelper
class TopBarFragment : GenericFragment() {
private var content: Content? = null
@@ -46,20 +54,9 @@ class TopBarFragment : GenericFragment() {
}
binding.setExportClickListener {
- if (content != null) {
- val filePath = content?.plainFilePath.orEmpty()
- plainFilePath = if (filePath.isEmpty()) content?.filePath.orEmpty() else filePath
- Log.i("[File Viewer] Plain file path is: $plainFilePath")
- if (plainFilePath.isNotEmpty()) {
- if (!FileUtils.openFileInThirdPartyApp(requireActivity(), plainFilePath)) {
- (requireActivity() as SnackBarActivity).showSnackBar(R.string.chat_message_no_app_found_to_handle_file_mime_type)
- if (plainFilePath != content?.filePath.orEmpty()) {
- Log.i("[File Viewer] No app to open plain file path: $plainFilePath, destroying it")
- FileUtils.deleteFile(plainFilePath)
- }
- plainFilePath = ""
- }
- }
+ val contentToExport = content
+ if (contentToExport != null) {
+ exportContent(contentToExport)
} else {
Log.e("[File Viewer] No Content set!")
}
@@ -89,4 +86,77 @@ class TopBarFragment : GenericFragment() {
content = c
binding.fileName.text = c.name
}
+
+ private fun exportContent(content: Content) {
+ lifecycleScope.launch {
+ var mediaStoreFilePath = ""
+ if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10) || PermissionHelper.get().hasWriteExternalStoragePermission()) {
+ Log.i("[File Viewer] Exporting image through Media Store API")
+ when (content.type) {
+ "image" -> {
+ val export = lifecycleScope.async {
+ Compatibility.addImageToMediaStore(requireContext(), content)
+ }
+ if (export.await()) {
+ Log.i("[File Viewer] Adding image ${content.name} to Media Store terminated: ${content.userData}")
+ mediaStoreFilePath = content.userData.toString()
+ } else {
+ Log.e("[File Viewer] Something went wrong while copying file to Media Store...")
+ }
+ }
+ "video" -> {
+ val export = lifecycleScope.async {
+ Compatibility.addVideoToMediaStore(requireContext(), content)
+ }
+ if (export.await()) {
+ Log.i("[File Viewer] Adding video ${content.name} to Media Store terminated: ${content.userData}")
+ mediaStoreFilePath = content.userData.toString()
+ } else {
+ Log.e("[File Viewer] Something went wrong while copying file to Media Store...")
+ }
+ }
+ "audio" -> {
+ val export = lifecycleScope.async {
+ Compatibility.addAudioToMediaStore(requireContext(), content)
+ }
+ if (export.await()) {
+ Log.i("[File Viewer] Adding audio ${content.name} to Media Store terminated: ${content.userData}")
+ mediaStoreFilePath = content.userData.toString()
+ } else {
+ Log.e("[File Viewer] Something went wrong while copying file to Media Store...")
+ }
+ }
+ else -> {
+ Log.w("[File Viewer] File ${content.name} isn't either an image, an audio file or a video, can't add it to the Media Store")
+ }
+ }
+ } else {
+ Log.w("[File Viewer] Can't export image through Media Store API (requires Android 10 or WRITE_EXTERNAL permission, using fallback method...")
+ }
+
+ withContext(Dispatchers.Main) {
+ if (mediaStoreFilePath.isEmpty()) {
+ Log.w("[File Viewer] Media store file path is empty, media store export failed?")
+
+ val filePath = content.plainFilePath.orEmpty()
+ plainFilePath = filePath.ifEmpty { content.filePath.orEmpty() }
+ Log.i("[File Viewer] Plain file path is: $plainFilePath")
+ if (plainFilePath.isNotEmpty()) {
+ if (!FileUtils.openFileInThirdPartyApp(requireActivity(), plainFilePath)) {
+ (requireActivity() as SnackBarActivity).showSnackBar(R.string.chat_message_no_app_found_to_handle_file_mime_type)
+ if (plainFilePath != content.filePath.orEmpty()) {
+ Log.i("[File Viewer] No app to open plain file path: $plainFilePath, destroying it")
+ FileUtils.deleteFile(plainFilePath)
+ }
+ plainFilePath = ""
+ }
+ }
+ } else {
+ plainFilePath = ""
+ Log.i("[File Viewer] Media store file path is: $mediaStoreFilePath")
+ FileUtils.openMediaStoreFile(requireActivity(), mediaStoreFilePath)
+ }
+ }
+ }
+ }
}
diff --git a/app/src/main/java/org/linphone/activities/main/files/viewmodels/FileViewerViewModel.kt b/app/src/main/java/org/linphone/activities/main/files/viewmodels/FileViewerViewModel.kt
index 8de31a6ce..45db338c4 100644
--- a/app/src/main/java/org/linphone/activities/main/files/viewmodels/FileViewerViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/files/viewmodels/FileViewerViewModel.kt
@@ -29,7 +29,12 @@ open class FileViewerViewModel(val content: Content) : ViewModel() {
private val deleteAfterUse: Boolean = content.isFileEncrypted
init {
- filePath = if (deleteAfterUse) content.plainFilePath else content.filePath.orEmpty()
+ filePath = if (deleteAfterUse) {
+ Log.i("[File Viewer] Content is encrypted, requesting plain file path")
+ content.plainFilePath
+ } else {
+ content.filePath.orEmpty()
+ }
}
override fun onCleared() {
diff --git a/app/src/main/java/org/linphone/activities/main/fragments/MasterFragment.kt b/app/src/main/java/org/linphone/activities/main/fragments/MasterFragment.kt
index a6f86e180..c342b2a9a 100644
--- a/app/src/main/java/org/linphone/activities/main/fragments/MasterFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/fragments/MasterFragment.kt
@@ -56,56 +56,55 @@ abstract class MasterFragment() {
}
sharedViewModel.accountRemoved.observe(
- viewLifecycleOwner,
- {
- Log.i("[Status Fragment] An account was removed, update default account state")
- val defaultAccount = coreContext.core.defaultAccount
- if (defaultAccount != null) {
- viewModel.updateDefaultAccountRegistrationStatus(defaultAccount.state)
- }
+ viewLifecycleOwner
+ ) {
+ Log.i("[Status Fragment] An account was removed, update default account state")
+ val defaultAccount = coreContext.core.defaultAccount
+ if (defaultAccount != null) {
+ viewModel.updateDefaultAccountRegistrationStatus(defaultAccount.state)
}
- )
+ }
binding.setMenuClickListener {
sharedViewModel.toggleDrawerEvent.value = Event(true)
diff --git a/app/src/main/java/org/linphone/activities/main/history/adapters/CallLogsListAdapter.kt b/app/src/main/java/org/linphone/activities/main/history/adapters/CallLogsListAdapter.kt
index ae1e80203..ac0194cc1 100644
--- a/app/src/main/java/org/linphone/activities/main/history/adapters/CallLogsListAdapter.kt
+++ b/app/src/main/java/org/linphone/activities/main/history/adapters/CallLogsListAdapter.kt
@@ -65,7 +65,7 @@ class CallLogsListAdapter(
) : RecyclerView.ViewHolder(binding.root) {
fun bind(callLogGroup: GroupedCallLogData) {
with(binding) {
- val callLogViewModel = callLogGroup.lastCallLogViewModel
+ val callLogViewModel = callLogGroup.lastCallLogData
viewModel = callLogViewModel
lifecycleOwner = viewLifecycleOwner
@@ -73,15 +73,14 @@ class CallLogsListAdapter(
// This is for item selection through ListTopBarFragment
selectionListViewModel = selectionViewModel
selectionViewModel.isEditionEnabled.observe(
- viewLifecycleOwner,
- {
- position = adapterPosition
- }
- )
+ viewLifecycleOwner
+ ) {
+ position = bindingAdapterPosition
+ }
setClickListener {
if (selectionViewModel.isEditionEnabled.value == true) {
- selectionViewModel.onToggleSelect(adapterPosition)
+ selectionViewModel.onToggleSelect(bindingAdapterPosition)
} else {
startCallToEvent.value = Event(callLogGroup)
}
diff --git a/app/src/main/java/org/linphone/activities/main/history/data/CallLogData.kt b/app/src/main/java/org/linphone/activities/main/history/data/CallLogData.kt
new file mode 100644
index 000000000..72483c694
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/history/data/CallLogData.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.activities.main.history.data
+
+import java.text.SimpleDateFormat
+import java.util.*
+import org.linphone.R
+import org.linphone.contact.GenericContactData
+import org.linphone.core.Call
+import org.linphone.core.CallLog
+import org.linphone.utils.TimestampUtils
+
+class CallLogData(callLog: CallLog) : GenericContactData(callLog.remoteAddress) {
+ val statusIconResource: Int by lazy {
+ if (callLog.dir == Call.Dir.Incoming) {
+ if (callLog.status == Call.Status.Missed) {
+ R.drawable.call_status_missed
+ } else {
+ R.drawable.call_status_incoming
+ }
+ } else {
+ R.drawable.call_status_outgoing
+ }
+ }
+
+ val iconContentDescription: Int by lazy {
+ if (callLog.dir == Call.Dir.Incoming) {
+ if (callLog.status == Call.Status.Missed) {
+ R.string.content_description_missed_call
+ } else {
+ R.string.content_description_incoming_call
+ }
+ } else {
+ R.string.content_description_outgoing_call
+ }
+ }
+
+ val directionIconResource: Int by lazy {
+ if (callLog.dir == Call.Dir.Incoming) {
+ if (callLog.status == Call.Status.Missed) {
+ R.drawable.call_missed
+ } else {
+ R.drawable.call_incoming
+ }
+ } else {
+ R.drawable.call_outgoing
+ }
+ }
+
+ val duration: String by lazy {
+ val dateFormat = SimpleDateFormat(if (callLog.duration >= 3600) "HH:mm:ss" else "mm:ss", Locale.getDefault())
+ val cal = Calendar.getInstance()
+ cal[0, 0, 0, 0, 0] = callLog.duration
+ dateFormat.format(cal.time)
+ }
+
+ val date: String by lazy {
+ TimestampUtils.toString(callLog.startDate, shortDate = false, hideYear = false)
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/history/data/GroupedCallLogData.kt b/app/src/main/java/org/linphone/activities/main/history/data/GroupedCallLogData.kt
index 89b0d1716..ea76e8fe8 100644
--- a/app/src/main/java/org/linphone/activities/main/history/data/GroupedCallLogData.kt
+++ b/app/src/main/java/org/linphone/activities/main/history/data/GroupedCallLogData.kt
@@ -19,15 +19,14 @@
*/
package org.linphone.activities.main.history.data
-import org.linphone.activities.main.history.viewmodels.CallLogViewModel
import org.linphone.core.CallLog
class GroupedCallLogData(callLog: CallLog) {
var lastCallLog: CallLog = callLog
val callLogs = arrayListOf(callLog)
- val lastCallLogViewModel = CallLogViewModel(lastCallLog)
+ val lastCallLogData = CallLogData(lastCallLog)
fun destroy() {
- lastCallLogViewModel.destroy()
+ lastCallLogData.destroy()
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt
index dc2599280..825921125 100644
--- a/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt
@@ -70,7 +70,7 @@ class DetailCallLogFragment : GenericFragment() {
useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false
- viewModel.relatedCallLogs.value = callLogGroup.callLogs
+ viewModel.addRelatedCallLogs(callLogGroup.callLogs)
binding.setBackClickListener {
goBack()
@@ -99,50 +99,48 @@ class DetailCallLogFragment : GenericFragment() {
}
viewModel.startCallEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { callLog ->
- val address = callLog.remoteAddress
- if (coreContext.core.callsNb > 0) {
- Log.i("[History] Starting dialer with pre-filled URI ${address.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}")
- sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.masterCallLogsFragment)
+ viewLifecycleOwner
+ ) {
+ it.consume { callLog ->
+ val address = callLog.remoteAddress
+ if (coreContext.core.callsNb > 0) {
+ Log.i("[History] Starting dialer with pre-filled URI ${address.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}")
+ sharedViewModel.updateDialerAnimationsBasedOnDestination.value =
+ Event(R.id.masterCallLogsFragment)
- val args = Bundle()
- args.putString("URI", address.asStringUriOnly())
- args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer)
- args.putBoolean(
- "SkipAutoCallStart",
- true
- ) // If auto start call setting is enabled, ignore it
- navigateToDialer(args)
- } else {
- val localAddress = callLog.localAddress
- coreContext.startCall(address, localAddress = localAddress)
- }
+ val args = Bundle()
+ args.putString("URI", address.asStringUriOnly())
+ args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer)
+ args.putBoolean(
+ "SkipAutoCallStart",
+ true
+ ) // If auto start call setting is enabled, ignore it
+ navigateToDialer(args)
+ } else {
+ val localAddress = callLog.localAddress
+ coreContext.startCall(address, localAddress = localAddress)
}
}
- )
+ }
viewModel.chatRoomCreatedEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { chatRoom ->
- val args = Bundle()
- args.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly())
- args.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly())
- navigateToChatRoom(args)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { chatRoom ->
+ val args = Bundle()
+ args.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly())
+ args.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly())
+ navigateToChatRoom(args)
}
- )
+ }
viewModel.onErrorEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { messageResourceId ->
- (activity as MainActivity).showSnackBar(messageResourceId)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { messageResourceId ->
+ (activity as MainActivity).showSnackBar(messageResourceId)
}
- )
+ }
}
override fun goBack() {
diff --git a/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt b/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt
index b597d21e4..bbd2e0cfa 100644
--- a/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt
@@ -101,46 +101,30 @@ class MasterCallLogsFragment : MasterFragment
- if (listViewModel.missedCallLogsSelected.value == false) {
- adapter.submitList(callLogs)
- }
+ viewLifecycleOwner
+ ) { callLogs ->
+ if (listViewModel.missedCallLogsSelected.value == false) {
+ adapter.submitList(callLogs)
}
- )
+ }
listViewModel.missedCallLogs.observe(
- viewLifecycleOwner,
- { callLogs ->
- if (listViewModel.missedCallLogsSelected.value == true) {
- adapter.submitList(callLogs)
- }
+ viewLifecycleOwner
+ ) { callLogs ->
+ if (listViewModel.missedCallLogsSelected.value == true) {
+ adapter.submitList(callLogs)
}
- )
+ }
listViewModel.missedCallLogsSelected.observe(
- viewLifecycleOwner,
- {
- if (it) {
- adapter.submitList(listViewModel.missedCallLogs.value)
- } else {
- adapter.submitList(listViewModel.callLogs.value)
- }
+ viewLifecycleOwner
+ ) {
+ if (it) {
+ adapter.submitList(listViewModel.missedCallLogs.value)
+ } else {
+ adapter.submitList(listViewModel.callLogs.value)
}
- )
+ }
listViewModel.contactsUpdatedEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- adapter.notifyDataSetChanged()
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ adapter.notifyDataSetChanged()
}
- )
+ }
adapter.selectedCallLogEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { callLog ->
- sharedViewModel.selectedCallLogGroup.value = callLog
- navigateToCallHistory(binding.slidingPane)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { callLog ->
+ sharedViewModel.selectedCallLogGroup.value = callLog
+ navigateToCallHistory(binding.slidingPane)
}
- )
+ }
adapter.startCallToEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { callLogGroup ->
- val remoteAddress = callLogGroup.lastCallLog.remoteAddress
- if (coreContext.core.callsNb > 0) {
- Log.i("[History] Starting dialer with pre-filled URI ${remoteAddress.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}")
- sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.masterCallLogsFragment)
- val args = Bundle()
- args.putString("URI", remoteAddress.asStringUriOnly())
- args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer)
- args.putBoolean("SkipAutoCallStart", true) // If auto start call setting is enabled, ignore it
- navigateToDialer(args)
- } else {
- val localAddress = callLogGroup.lastCallLog.localAddress
- coreContext.startCall(remoteAddress, localAddress = localAddress)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { callLogGroup ->
+ val remoteAddress = callLogGroup.lastCallLog.remoteAddress
+ if (coreContext.core.callsNb > 0) {
+ Log.i("[History] Starting dialer with pre-filled URI ${remoteAddress.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}")
+ sharedViewModel.updateDialerAnimationsBasedOnDestination.value =
+ Event(R.id.masterCallLogsFragment)
+ val args = Bundle()
+ args.putString("URI", remoteAddress.asStringUriOnly())
+ args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer)
+ // If auto start call setting is enabled, ignore it
+ args.putBoolean("SkipAutoCallStart", true)
+ navigateToDialer(args)
+ } else {
+ val localAddress = callLogGroup.lastCallLog.localAddress
+ coreContext.startCall(remoteAddress, localAddress = localAddress)
}
}
- )
+ }
binding.setAllCallLogsToggleClickListener {
listViewModel.missedCallLogsSelected.value = false
diff --git a/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt b/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt
index f5529312a..a5cabce8b 100644
--- a/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt
@@ -22,17 +22,16 @@ package org.linphone.activities.main.history.viewmodels
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
-import java.text.SimpleDateFormat
-import java.util.*
import kotlin.collections.ArrayList
+import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
+import org.linphone.activities.main.history.data.CallLogData
import org.linphone.contact.GenericContactViewModel
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
-import org.linphone.utils.TimestampUtils
class CallLogViewModelFactory(private val callLog: CallLog) :
ViewModelProvider.NewInstanceFactory() {
@@ -48,53 +47,6 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
LinphoneUtils.getDisplayableAddress(callLog.remoteAddress)
}
- val statusIconResource: Int by lazy {
- if (callLog.dir == Call.Dir.Incoming) {
- if (callLog.status == Call.Status.Missed) {
- R.drawable.call_status_missed
- } else {
- R.drawable.call_status_incoming
- }
- } else {
- R.drawable.call_status_outgoing
- }
- }
-
- val iconContentDescription: Int by lazy {
- if (callLog.dir == Call.Dir.Incoming) {
- if (callLog.status == Call.Status.Missed) {
- R.string.content_description_missed_call
- } else {
- R.string.content_description_incoming_call
- }
- } else {
- R.string.content_description_outgoing_call
- }
- }
-
- val directionIconResource: Int by lazy {
- if (callLog.dir == Call.Dir.Incoming) {
- if (callLog.status == Call.Status.Missed) {
- R.drawable.call_missed
- } else {
- R.drawable.call_incoming
- }
- } else {
- R.drawable.call_outgoing
- }
- }
-
- val duration: String by lazy {
- val dateFormat = SimpleDateFormat(if (callLog.duration >= 3600) "HH:mm:ss" else "mm:ss", Locale.getDefault())
- val cal = Calendar.getInstance()
- cal[0, 0, 0, 0, 0] = callLog.duration
- dateFormat.format(cal.time)
- }
-
- val date: String by lazy {
- TimestampUtils.toString(callLog.startDate, shortDate = false, hideYear = false)
- }
-
val startCallEvent: MutableLiveData> by lazy {
MutableLiveData>()
}
@@ -109,7 +61,16 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
val secureChatAllowed = contact.value?.friend?.getPresenceModelForUriOrTel(peerSipUri)?.hasCapability(FriendCapability.LimeX3Dh) ?: false
- val relatedCallLogs = MutableLiveData>()
+ val relatedCallLogs = MutableLiveData>()
+
+ private val listener = object : CoreListenerStub() {
+ override fun onCallLogUpdated(core: Core, log: CallLog) {
+ if (callLog.remoteAddress.weakEqual(log.remoteAddress) && callLog.localAddress.weakEqual(log.localAddress)) {
+ Log.i("[History Detail] New call log for ${callLog.remoteAddress.asStringUriOnly()} with local address ${callLog.localAddress.asStringUriOnly()}")
+ addRelatedCallLogs(arrayListOf(log))
+ }
+ }
+ }
private val chatRoomListener = object : ChatRoomListenerStub() {
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
@@ -126,14 +87,19 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
init {
waitForChatRoomCreation.value = false
+
+ coreContext.core.addListener(listener)
}
override fun onCleared() {
+ coreContext.core.removeListener(listener)
destroy()
+
super.onCleared()
}
fun destroy() {
+ relatedCallLogs.value.orEmpty().forEach(CallLogData::destroy)
}
fun startCall() {
@@ -157,11 +123,15 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
}
}
- fun getCallsHistory(): ArrayList {
- val callsHistory = ArrayList()
- for (callLog in relatedCallLogs.value.orEmpty()) {
- callsHistory.add(CallLogViewModel(callLog))
+ fun addRelatedCallLogs(logs: ArrayList) {
+ val callsHistory = ArrayList()
+
+ // We assume new logs are more recent than the ones we already have, so we add them first
+ for (log in logs) {
+ callsHistory.add(CallLogData(log))
}
- return callsHistory
+ callsHistory.addAll(relatedCallLogs.value.orEmpty())
+
+ relatedCallLogs.value = callsHistory
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/recordings/adapters/RecordingsListAdapter.kt b/app/src/main/java/org/linphone/activities/main/recordings/adapters/RecordingsListAdapter.kt
index 43b2c48ad..839271c38 100644
--- a/app/src/main/java/org/linphone/activities/main/recordings/adapters/RecordingsListAdapter.kt
+++ b/app/src/main/java/org/linphone/activities/main/recordings/adapters/RecordingsListAdapter.kt
@@ -69,12 +69,12 @@ class RecordingsListAdapter(
lifecycleOwner = viewLifecycleOwner
// This is for item selection through ListTopBarFragment
- position = adapterPosition
+ position = bindingAdapterPosition
selectionListViewModel = selectionViewModel
setClickListener {
if (selectionViewModel.isEditionEnabled.value == true) {
- selectionViewModel.onToggleSelect(adapterPosition)
+ selectionViewModel.onToggleSelect(bindingAdapterPosition)
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/recordings/data/RecordingData.kt b/app/src/main/java/org/linphone/activities/main/recordings/data/RecordingData.kt
index d9093af29..fa3e9ced2 100644
--- a/app/src/main/java/org/linphone/activities/main/recordings/data/RecordingData.kt
+++ b/app/src/main/java/org/linphone/activities/main/recordings/data/RecordingData.kt
@@ -164,8 +164,9 @@ class RecordingData(val path: String, private val recordingListener: RecordingLi
}
private fun initPlayer() {
- // Use speaker sound card to play recordings, otherwise use earpiece
+ // In case no headphones/headset is connected, use speaker sound card to play recordings, otherwise use earpiece
// If none are available, default one will be used
+ var headphonesCard: String? = null
var speakerCard: String? = null
var earpieceCard: String? = null
for (device in coreContext.core.audioDevices) {
@@ -174,11 +175,14 @@ class RecordingData(val path: String, private val recordingListener: RecordingLi
speakerCard = device.id
} else if (device.type == AudioDevice.Type.Earpiece) {
earpieceCard = device.id
+ } else if (device.type == AudioDevice.Type.Headphones || device.type == AudioDevice.Type.Headset) {
+ headphonesCard = device.id
}
}
}
+ Log.i("[Recording VM] Found headset/headphones sound card [$headphonesCard], speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]")
- val localPlayer = coreContext.core.createLocalPlayer(speakerCard ?: earpieceCard, null, null)
+ val localPlayer = coreContext.core.createLocalPlayer(headphonesCard ?: speakerCard ?: earpieceCard, null, null)
if (localPlayer != null) player = localPlayer
else Log.e("[Recording VM] Couldn't create local player!")
player.addListener(listener)
diff --git a/app/src/main/java/org/linphone/activities/main/recordings/fragments/RecordingsFragment.kt b/app/src/main/java/org/linphone/activities/main/recordings/fragments/RecordingsFragment.kt
index 582debc2d..d82ede726 100644
--- a/app/src/main/java/org/linphone/activities/main/recordings/fragments/RecordingsFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/recordings/fragments/RecordingsFragment.kt
@@ -29,6 +29,7 @@ import org.linphone.activities.main.fragments.MasterFragment
import org.linphone.activities.main.recordings.adapters.RecordingsListAdapter
import org.linphone.activities.main.recordings.data.RecordingData
import org.linphone.activities.main.recordings.viewmodels.RecordingsViewModel
+import org.linphone.core.tools.Log
import org.linphone.databinding.RecordingsFragmentBinding
import org.linphone.utils.AppUtils
import org.linphone.utils.RecyclerViewHeaderDecoration
@@ -69,11 +70,10 @@ class RecordingsFragment : MasterFragment
- adapter.submitList(recordings)
- }
- )
+ viewLifecycleOwner
+ ) { recordings ->
+ adapter.submitList(recordings)
+ }
binding.setBackClickListener { goBack() }
@@ -111,4 +111,13 @@ class RecordingsFragment : MasterFragment()
diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt
index df8623f9c..cb18d3a35 100644
--- a/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt
@@ -63,32 +63,30 @@ class AccountSettingsFragment : GenericSettingFragment
- val clipboard =
- requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
- val clip = ClipData.newPlainText("Logs url", url)
- clipboard.setPrimaryClip(clip)
+ viewLifecycleOwner
+ ) {
+ it.consume { url ->
+ val clipboard =
+ requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText("Logs url", url)
+ clipboard.setPrimaryClip(clip)
- val activity = requireActivity() as MainActivity
- activity.showSnackBar(R.string.logs_url_copied_to_clipboard)
+ val activity = requireActivity() as MainActivity
+ activity.showSnackBar(R.string.logs_url_copied_to_clipboard)
- AppUtils.shareUploadedLogsUrl(activity, url)
- }
+ AppUtils.shareUploadedLogsUrl(activity, url)
}
- )
+ }
viewModel.uploadErrorEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- val activity = requireActivity() as MainActivity
- activity.showSnackBar(R.string.logs_upload_failure)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ val activity = requireActivity() as MainActivity
+ activity.showSnackBar(R.string.logs_upload_failure)
}
- )
+ }
viewModel.resetCompleteEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- val activity = requireActivity() as MainActivity
- activity.showSnackBar(R.string.logs_reset_complete)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ val activity = requireActivity() as MainActivity
+ activity.showSnackBar(R.string.logs_reset_complete)
}
- )
+ }
viewModel.setNightModeEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { value ->
- AppCompatDelegate.setDefaultNightMode(
- when (value) {
- 0 -> AppCompatDelegate.MODE_NIGHT_NO
- 1 -> AppCompatDelegate.MODE_NIGHT_YES
- else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
- }
- )
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { value ->
+ AppCompatDelegate.setDefaultNightMode(
+ when (value) {
+ 0 -> AppCompatDelegate.MODE_NIGHT_NO
+ 1 -> AppCompatDelegate.MODE_NIGHT_YES
+ else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
+ }
+ )
}
- )
+ }
viewModel.backgroundModeEnabled.value = !DeviceUtils.isAppUserRestricted(requireContext())
viewModel.goToBatterySettingsEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- try {
- val intent = Intent("android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS")
- startActivity(intent)
- } catch (anfe: ActivityNotFoundException) {
- Log.e("[Advanced Settings] ActivityNotFound exception: ", anfe)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ try {
+ val intent = Intent("android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS")
+ startActivity(intent)
+ } catch (anfe: ActivityNotFoundException) {
+ Log.e("[Advanced Settings] ActivityNotFound exception: ", anfe)
}
}
- )
+ }
viewModel.powerManagerSettingsVisibility.value = PowerManagerUtils.getDevicePowerManagerIntent(requireContext()) != null
viewModel.goToPowerManagerSettingsEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- val intent = PowerManagerUtils.getDevicePowerManagerIntent(requireActivity())
- if (intent != null) {
- try {
- startActivity(intent)
- } catch (se: SecurityException) {
- Log.e("[Advanced Settings] Security exception: ", se)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ val intent = PowerManagerUtils.getDevicePowerManagerIntent(requireActivity())
+ if (intent != null) {
+ try {
+ startActivity(intent)
+ } catch (se: SecurityException) {
+ Log.e("[Advanced Settings] Security exception: ", se)
}
}
}
- )
+ }
viewModel.goToAndroidSettingsEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- val intent = Intent()
- intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
- intent.addCategory(Intent.CATEGORY_DEFAULT)
- intent.data = Uri.parse("package:${requireContext().packageName}")
- intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
- ContextCompat.startActivity(requireContext(), intent, null)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ val intent = Intent()
+ intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
+ intent.addCategory(Intent.CATEGORY_DEFAULT)
+ intent.data = Uri.parse("package:${requireContext().packageName}")
+ intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
+ ContextCompat.startActivity(requireContext(), intent, null)
}
- )
+ }
}
override fun goBack() {
diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/AudioSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/AudioSettingsFragment.kt
index 1f85d7fdd..2ed0343eb 100644
--- a/app/src/main/java/org/linphone/activities/main/settings/fragments/AudioSettingsFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/AudioSettingsFragment.kt
@@ -54,24 +54,22 @@ class AudioSettingsFragment : GenericSettingFragment() {
private lateinit var viewModel: CallSettingsViewModel
@@ -57,80 +55,75 @@ class CallSettingsFragment : GenericSettingFragment
binding.setBackClickListener { goBack() }
viewModel.systemWideOverlayEnabledEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- if (!Compatibility.canDrawOverlay(requireContext())) {
- val intent = Intent("android.settings.action.MANAGE_OVERLAY_PERMISSION", Uri.parse("package:${requireContext().packageName}"))
- startActivityForResult(intent, 0)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ if (!Compatibility.canDrawOverlay(requireContext())) {
+ val intent = Intent(
+ "android.settings.action.MANAGE_OVERLAY_PERMISSION",
+ Uri.parse("package:${requireContext().packageName}")
+ )
+ startActivityForResult(intent, 0)
}
}
- )
+ }
viewModel.goToAndroidNotificationSettingsEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- if (Build.VERSION.SDK_INT >= Version.API26_O_80) {
- val i = Intent()
- i.action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
- i.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
- i.putExtra(
- Settings.EXTRA_CHANNEL_ID,
- getString(R.string.notification_channel_service_id)
- )
- i.addCategory(Intent.CATEGORY_DEFAULT)
- i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
- i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
- startActivity(i)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ if (Build.VERSION.SDK_INT >= Version.API26_O_80) {
+ val i = Intent()
+ i.action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
+ i.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
+ i.putExtra(
+ Settings.EXTRA_CHANNEL_ID,
+ getString(R.string.notification_channel_service_id)
+ )
+ i.addCategory(Intent.CATEGORY_DEFAULT)
+ i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
+ i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+ startActivity(i)
}
}
- )
+ }
viewModel.enableTelecomManagerEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- if (!PermissionHelper.get().hasTelecomManagerPermissions()) {
- val permissions = arrayOf(
- Manifest.permission.READ_PHONE_NUMBERS,
- Manifest.permission.MANAGE_OWN_CALLS
- )
- requestPermissions(permissions, 1)
- } else if (!TelecomHelper.exists()) {
- corePreferences.useTelecomManager = true
- Log.w("[Telecom Helper] Doesn't exists yet, creating it")
- TelecomHelper.create(requireContext())
- updateTelecomManagerAccount()
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ if (!Compatibility.hasTelecomManagerPermissions(requireContext())) {
+ Compatibility.requestTelecomManagerPermissions(requireActivity(), 1)
+ } else if (!TelecomHelper.exists()) {
+ corePreferences.useTelecomManager = true
+ Log.w("[Telecom Helper] Doesn't exists yet, creating it")
+ TelecomHelper.create(requireContext())
+ updateTelecomManagerAccount()
}
}
- )
+ }
viewModel.goToAndroidNotificationSettingsEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- if (Build.VERSION.SDK_INT >= Version.API26_O_80) {
- val i = Intent()
- i.action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
- i.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
- i.putExtra(
- Settings.EXTRA_CHANNEL_ID,
- getString(R.string.notification_channel_service_id)
- )
- i.addCategory(Intent.CATEGORY_DEFAULT)
- i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
- i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
- startActivity(i)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ if (Build.VERSION.SDK_INT >= Version.API26_O_80) {
+ val i = Intent()
+ i.action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
+ i.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
+ i.putExtra(
+ Settings.EXTRA_CHANNEL_ID,
+ getString(R.string.notification_channel_service_id)
+ )
+ i.addCategory(Intent.CATEGORY_DEFAULT)
+ i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
+ i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+ startActivity(i)
}
}
- )
+ }
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/ChatSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/ChatSettingsFragment.kt
index e04d7f2a3..2a89ac47c 100644
--- a/app/src/main/java/org/linphone/activities/main/settings/fragments/ChatSettingsFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/ChatSettingsFragment.kt
@@ -50,39 +50,37 @@ class ChatSettingsFragment : GenericSettingFragment
binding.setBackClickListener { goBack() }
viewModel.launcherShortcutsEvent.observe(
- viewLifecycleOwner,
- {
- it.consume { newValue ->
- if (newValue) {
- Compatibility.createShortcutsToChatRooms(requireContext())
- } else {
- Compatibility.removeShortcuts(requireContext())
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { newValue ->
+ if (newValue) {
+ Compatibility.createShortcutsToChatRooms(requireContext())
+ } else {
+ Compatibility.removeShortcuts(requireContext())
}
}
- )
+ }
viewModel.goToAndroidNotificationSettingsEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- if (Build.VERSION.SDK_INT >= Version.API26_O_80) {
- val i = Intent()
- i.action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
- i.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
- i.putExtra(
- Settings.EXTRA_CHANNEL_ID,
- getString(R.string.notification_channel_chat_id)
- )
- i.addCategory(Intent.CATEGORY_DEFAULT)
- i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
- i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
- startActivity(i)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ if (Build.VERSION.SDK_INT >= Version.API26_O_80) {
+ val i = Intent()
+ i.action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
+ i.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
+ i.putExtra(
+ Settings.EXTRA_CHANNEL_ID,
+ getString(R.string.notification_channel_chat_id)
+ )
+ i.addCategory(Intent.CATEGORY_DEFAULT)
+ i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
+ i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
+ startActivity(i)
}
}
- )
+ }
}
override fun goBack() {
diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/ContactsSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/ContactsSettingsFragment.kt
index acb9a6556..4d89df1a4 100644
--- a/app/src/main/java/org/linphone/activities/main/settings/fragments/ContactsSettingsFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/ContactsSettingsFragment.kt
@@ -51,30 +51,28 @@ class ContactsSettingsFragment : GenericSettingFragment
- if (newValue) {
- Compatibility.createShortcutsToContacts(requireContext())
- } else {
- Compatibility.removeShortcuts(requireContext())
- if (corePreferences.chatRoomShortcuts) {
- Compatibility.createShortcutsToChatRooms(requireContext())
- }
+ viewLifecycleOwner
+ ) {
+ it.consume { newValue ->
+ if (newValue) {
+ Compatibility.createShortcutsToContacts(requireContext())
+ } else {
+ Compatibility.removeShortcuts(requireContext())
+ if (corePreferences.chatRoomShortcuts) {
+ Compatibility.createShortcutsToChatRooms(requireContext())
}
}
}
- )
+ }
viewModel.askWriteContactsPermissionForPresenceStorageEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- Log.i("[Contacts Settings] Asking for WRITE_CONTACTS permission to be able to store presence")
- requestPermissions(arrayOf(android.Manifest.permission.WRITE_CONTACTS), 1)
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ Log.i("[Contacts Settings] Asking for WRITE_CONTACTS permission to be able to store presence")
+ requestPermissions(arrayOf(android.Manifest.permission.WRITE_CONTACTS), 1)
}
- )
+ }
if (!PermissionHelper.required(requireContext()).hasReadContactsPermission()) {
Log.i("[Contacts Settings] Asking for READ_CONTACTS permission")
diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/SettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/SettingsFragment.kt
index 800e9af57..6395507b9 100644
--- a/app/src/main/java/org/linphone/activities/main/settings/fragments/SettingsFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/settings/fragments/SettingsFragment.kt
@@ -69,39 +69,37 @@ class SettingsFragment : SecureFragment() {
// Account settings loading can take some time, so wait until it is ready before opening the pane
sharedViewModel.accountSettingsFragmentOpenedEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- binding.slidingPane.openPane()
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ binding.slidingPane.openPane()
}
- )
+ }
sharedViewModel.closeSlidingPaneEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- if (!binding.slidingPane.closePane()) {
- goBack()
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ if (!binding.slidingPane.closePane()) {
+ goBack()
}
}
- )
+ }
sharedViewModel.layoutChangedEvent.observe(
- viewLifecycleOwner,
- {
- it.consume {
- sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
- if (binding.slidingPane.isSlideable) {
- val navHostFragment = childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment
- if (navHostFragment.navController.currentDestination?.id == R.id.emptySettingsFragment) {
- Log.i("[Settings] Foldable device has been folded, closing side pane with empty fragment")
- binding.slidingPane.closePane()
- }
+ viewLifecycleOwner
+ ) {
+ it.consume {
+ sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
+ if (binding.slidingPane.isSlideable) {
+ val navHostFragment =
+ childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment
+ if (navHostFragment.navController.currentDestination?.id == R.id.emptySettingsFragment) {
+ Log.i("[Settings] Foldable device has been folded, closing side pane with empty fragment")
+ binding.slidingPane.closePane()
}
}
}
- )
+ }
binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
/* End of shared view model & sliding pane related */
@@ -112,12 +110,11 @@ class SettingsFragment : SecureFragment() {
binding.setBackClickListener { goBack() }
sharedViewModel.accountRemoved.observe(
- viewLifecycleOwner,
- {
- Log.i("[Settings] Account removed, update accounts list")
- viewModel.updateAccountsList()
- }
- )
+ viewLifecycleOwner
+ ) {
+ Log.i("[Settings] Account removed, update accounts list")
+ viewModel.updateAccountsList()
+ }
val identity = arguments?.getString("Identity")
if (identity != null) {
diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AccountSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AccountSettingsViewModel.kt
index 6841ff5a3..81760768b 100644
--- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AccountSettingsViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AccountSettingsViewModel.kt
@@ -192,7 +192,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
val disableListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
val params = account.params.clone()
- params.registerEnabled = !newValue
+ params.isRegisterEnabled = !newValue
account.params = params
}
}
@@ -239,7 +239,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
}
val params = account.params.clone()
- params.registerEnabled = false
+ params.isRegisterEnabled = false
account.params = params
if (!registered) {
@@ -288,7 +288,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
val outboundProxyListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
val params = account.params.clone()
- params.outboundProxyEnabled = newValue
+ params.isOutboundProxyEnabled = newValue
account.params = params
}
}
@@ -297,8 +297,15 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
val stunServerListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) {
val params = account.params.clone()
- params.natPolicy?.stunServer = newValue
- if (newValue.isEmpty()) ice.value = false
+ if (params.natPolicy == null) {
+ Log.w("[Account Settings] No NAT Policy object in account params yet")
+ val natPolicy = core.createNatPolicy()
+ natPolicy.stunServer = newValue
+ params.natPolicy = natPolicy
+ } else {
+ params.natPolicy?.stunServer = newValue
+ if (newValue.isEmpty()) ice.value = false
+ }
stunServer.value = newValue
account.params = params
}
@@ -308,7 +315,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
val iceListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
val params = account.params.clone()
- params.natPolicy?.enableIce(newValue)
+ params.natPolicy?.isIceEnabled = newValue
account.params = params
}
}
@@ -370,7 +377,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
val escapePlusListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
val params = account.params.clone()
- params.dialEscapePlusEnabled = newValue
+ params.isDialEscapePlusEnabled = newValue
account.params = params
}
}
@@ -424,19 +431,19 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
userName.value = params.identityAddress?.username
userId.value = account.findAuthInfo()?.userid
domain.value = params.identityAddress?.domain
- disable.value = !params.registerEnabled
+ disable.value = !params.isRegisterEnabled
pushNotification.value = params.pushNotificationAllowed
pushNotificationsAvailable.value = core.isPushNotificationAvailable
proxy.value = params.serverAddress?.asStringUriOnly()
- outboundProxy.value = params.outboundProxyEnabled
+ outboundProxy.value = params.isOutboundProxyEnabled
stunServer.value = params.natPolicy?.stunServer
- ice.value = params.natPolicy?.iceEnabled()
+ ice.value = params.natPolicy?.isIceEnabled
avpf.value = params.avpfMode == AVPFMode.Enabled
avpfRrInterval.value = params.avpfRrInterval
expires.value = params.expires
prefix.value = params.internationalPrefix
dialPrefix.value = params.useInternationalPrefixForCallsAndChats
- escapePlus.value = params.dialEscapePlusEnabled
+ escapePlus.value = params.isDialEscapePlusEnabled
}
private fun initTransportList() {
diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AdvancedSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AdvancedSettingsViewModel.kt
index ba78aaa23..c177e7386 100644
--- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AdvancedSettingsViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AdvancedSettingsViewModel.kt
@@ -32,8 +32,8 @@ import org.linphone.mediastream.Version
import org.linphone.utils.Event
class AdvancedSettingsViewModel : LogsUploadViewModel() {
- protected val prefs = corePreferences
- protected val core = coreContext.core
+ private val prefs = corePreferences
+ private val core = coreContext.core
val debugModeListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AudioSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AudioSettingsViewModel.kt
index 16551c4cb..8c684ef7d 100644
--- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AudioSettingsViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AudioSettingsViewModel.kt
@@ -41,7 +41,7 @@ class AudioSettingsViewModel : GenericSettingsViewModel() {
val echoCancellationListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
- core.enableEchoCancellation(newValue)
+ core.isEchoCancellationEnabled = newValue
}
}
val echoCancellation = MutableLiveData()
@@ -81,7 +81,7 @@ class AudioSettingsViewModel : GenericSettingsViewModel() {
val adaptiveRateControlListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
- core.enableAdaptiveRateControl(newValue)
+ core.isAdaptiveRateControlEnabled = newValue
}
}
val adaptiveRateControl = MutableLiveData()
@@ -110,6 +110,13 @@ class AudioSettingsViewModel : GenericSettingsViewModel() {
val outputAudioDeviceLabels = MutableLiveData>()
private val outputAudioDeviceValues = MutableLiveData>()
+ val preferBluetoothDevicesListener = object : SettingListenerStub() {
+ override fun onBoolValueChanged(newValue: Boolean) {
+ prefs.routeAudioToBluetoothIfAvailable = newValue
+ }
+ }
+ val preferBluetoothDevices = MutableLiveData()
+
val codecBitrateListener = object : SettingListenerStub() {
override fun onListValueChanged(position: Int) {
for (payloadType in core.audioPayloadTypes) {
@@ -146,14 +153,15 @@ class AudioSettingsViewModel : GenericSettingsViewModel() {
val audioCodecs = MutableLiveData>()
init {
- echoCancellation.value = core.echoCancellationEnabled()
- adaptiveRateControl.value = core.adaptiveRateControlEnabled()
- echoCalibration.value = if (core.echoCancellationEnabled()) {
+ echoCancellation.value = core.isEchoCancellationEnabled
+ adaptiveRateControl.value = core.isAdaptiveRateControlEnabled
+ echoCalibration.value = if (core.isEchoCancellationEnabled) {
prefs.getString(R.string.audio_settings_echo_cancellation_calibration_value).format(prefs.echoCancellerCalibration)
} else {
prefs.getString(R.string.audio_settings_echo_canceller_calibration_summary)
}
echoTesterStatus.value = prefs.getString(R.string.audio_settings_echo_tester_summary)
+ preferBluetoothDevices.value = prefs.routeAudioToBluetoothIfAvailable
initInputAudioDevicesList()
initOutputAudioDevicesList()
initCodecBitrateList()
diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt
index 47f3ef501..f95ab222e 100644
--- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt
@@ -20,22 +20,40 @@
package org.linphone.activities.main.settings.viewmodels
import androidx.lifecycle.MutableLiveData
+import java.io.File
+import java.util.*
+import kotlin.collections.ArrayList
import org.linphone.R
import org.linphone.activities.main.settings.SettingListenerStub
import org.linphone.core.MediaEncryption
import org.linphone.core.tools.Log
import org.linphone.mediastream.Version
import org.linphone.telecom.TelecomHelper
+import org.linphone.utils.AppUtils
import org.linphone.utils.Event
class CallSettingsViewModel : GenericSettingsViewModel() {
val deviceRingtoneListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
- core.ring = if (newValue) null else prefs.ringtonePath
+ core.ring = if (newValue) null else prefs.defaultRingtonePath
}
}
val deviceRingtone = MutableLiveData()
+ val ringtoneListener = object : SettingListenerStub() {
+ override fun onListValueChanged(position: Int) {
+ if (position == 0) {
+ core.ring = null
+ } else {
+ core.ring = ringtoneValues[position]
+ }
+ }
+ }
+ val ringtoneIndex = MutableLiveData()
+ val ringtoneLabels = MutableLiveData>()
+ private val ringtoneValues = arrayListOf()
+ val showRingtonesList = MutableLiveData()
+
val vibrateOnIncomingCallListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
core.isVibrationOnIncomingCallEnabled = newValue
@@ -73,6 +91,9 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
TelecomHelper.get().removeAccount()
TelecomHelper.get().destroy()
TelecomHelper.destroy()
+
+ Log.w("[Call Settings] Disabling Telecom Manager auto-enable")
+ prefs.manuallyDisabledTelecomManager = true
}
prefs.useTelecomManager = newValue
}
@@ -209,7 +230,10 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
val goToAndroidNotificationSettingsEvent = MutableLiveData>()
init {
+ initRingtonesList()
deviceRingtone.value = core.ring == null
+ showRingtonesList.value = prefs.showAllRingtones
+
vibrateOnIncomingCall.value = core.isVibrationOnIncomingCallEnabled
initEncryptionList()
@@ -235,6 +259,28 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
pauseCallsWhenAudioFocusIsLost.value = prefs.pauseCallsWhenAudioFocusIsLost
}
+ private fun initRingtonesList() {
+ val labels = arrayListOf()
+ labels.add(AppUtils.getString(R.string.call_settings_device_ringtone_title))
+ ringtoneValues.add("")
+
+ val directory = File(prefs.ringtonesPath)
+ val files = directory.listFiles()
+ for (ringtone in files.orEmpty()) {
+ if (ringtone.absolutePath.endsWith(".mkv")) {
+ val name = ringtone.name
+ .substringBefore(".")
+ .replace("_", " ")
+ .capitalize(Locale.getDefault())
+ labels.add(name)
+ ringtoneValues.add(ringtone.absolutePath)
+ }
+ }
+
+ ringtoneLabels.value = labels
+ ringtoneIndex.value = if (core.ring == null) 0 else ringtoneValues.indexOf(core.ring)
+ }
+
private fun initEncryptionList() {
val labels = arrayListOf()
diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt
index 01f78ccf3..4b8de0353 100644
--- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt
@@ -35,7 +35,7 @@ class ChatSettingsViewModel : GenericSettingsViewModel() {
val fileSharingUrlListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) {
- core.logCollectionUploadServerUrl = newValue
+ core.fileTransferServer = newValue
}
}
val fileSharingUrl = MutableLiveData()
diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ContactsSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ContactsSettingsViewModel.kt
index 805279621..41aac8d87 100644
--- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ContactsSettingsViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ContactsSettingsViewModel.kt
@@ -33,7 +33,7 @@ class ContactsSettingsViewModel : GenericSettingsViewModel() {
val friendListSubscribeListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
- core.enableFriendListSubscription(newValue)
+ core.isFriendListSubscriptionEnabled = newValue
}
}
val friendListSubscribe = MutableLiveData()
diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/NetworkSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/NetworkSettingsViewModel.kt
index 1a07e8736..c282b006e 100644
--- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/NetworkSettingsViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/NetworkSettingsViewModel.kt
@@ -26,14 +26,14 @@ import org.linphone.activities.main.settings.SettingListenerStub
class NetworkSettingsViewModel : GenericSettingsViewModel() {
val wifiOnlyListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
- core.enableWifiOnly(newValue)
+ core.isWifiOnlyEnabled = newValue
}
}
val wifiOnly = MutableLiveData()
val allowIpv6Listener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
- core.enableIpv6(newValue)
+ core.isIpv6Enabled = newValue
}
}
val allowIpv6 = MutableLiveData()
@@ -59,8 +59,8 @@ class NetworkSettingsViewModel : GenericSettingsViewModel() {
val sipPort = MutableLiveData()
init {
- wifiOnly.value = core.wifiOnlyEnabled()
- allowIpv6.value = core.ipv6Enabled()
+ wifiOnly.value = core.isWifiOnlyEnabled
+ allowIpv6.value = core.isIpv6Enabled
randomPorts.value = getTransportPort() == -1
sipPort.value = getTransportPort()
}
diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/TunnelSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/TunnelSettingsViewModel.kt
index f07177204..3822fe5fa 100644
--- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/TunnelSettingsViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/TunnelSettingsViewModel.kt
@@ -52,7 +52,7 @@ class TunnelSettingsViewModel : GenericSettingsViewModel() {
val useDualModeListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
val tunnel = core.tunnel
- tunnel?.enableDualMode(newValue)
+ tunnel?.isDualModeEnabled = newValue
}
}
val useDualMode = MutableLiveData()
@@ -96,7 +96,7 @@ class TunnelSettingsViewModel : GenericSettingsViewModel() {
hostnameUrl.value = config.host
port.value = config.port
- useDualMode.value = tunnel?.dualModeEnabled()
+ useDualMode.value = tunnel?.isDualModeEnabled
hostnameUrl2.value = config.host2
port2.value = config.port2
diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/VideoSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/VideoSettingsViewModel.kt
index f8ed1bfc1..d871e341a 100644
--- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/VideoSettingsViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/VideoSettingsViewModel.kt
@@ -31,8 +31,8 @@ import org.linphone.core.tools.Log
class VideoSettingsViewModel : GenericSettingsViewModel() {
val enableVideoListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
- core.enableVideoCapture(newValue)
- core.enableVideoDisplay(newValue)
+ core.isVideoCaptureEnabled = newValue
+ core.isVideoDisplayEnabled = newValue
if (!newValue) {
tabletPreview.value = false
initiateCall.value = false
@@ -115,7 +115,7 @@ class VideoSettingsViewModel : GenericSettingsViewModel() {
val videoCodecs = MutableLiveData>()
init {
- enableVideo.value = core.videoEnabled() && core.videoSupported()
+ enableVideo.value = core.isVideoEnabled && core.videoSupported()
tabletPreview.value = prefs.videoPreview
isTablet.value = coreContext.context.resources.getBoolean(R.bool.isTablet)
initiateCall.value = core.videoActivationPolicy.automaticallyInitiate
diff --git a/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt b/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt
index 84183526d..3251193a1 100644
--- a/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt
@@ -31,6 +31,7 @@ import androidx.lifecycle.lifecycleScope
import java.io.File
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.activities.GenericFragment
import org.linphone.activities.assistant.AssistantActivity
@@ -67,12 +68,11 @@ class SideMenuFragment : GenericFragment() {
}
sharedViewModel.accountRemoved.observe(
- viewLifecycleOwner,
- {
- Log.i("[Side Menu] Account removed, update accounts list")
- viewModel.updateAccountsList()
- }
- )
+ viewLifecycleOwner
+ ) {
+ Log.i("[Side Menu] Account removed, update accounts list")
+ viewModel.updateAccountsList()
+ }
viewModel.accountsSettingsListener = object : SettingListenerStub() {
override fun onAccountClicked(identity: String) {
@@ -110,8 +110,15 @@ class SideMenuFragment : GenericFragment() {
}
binding.setQuitClickListener {
+ Log.i("[Side Menu] Quitting app")
requireActivity().finishAndRemoveTask()
- coreContext.stop()
+
+ if (!corePreferences.keepServiceAlive) {
+ Log.i("[Side Menu] Stopping Core")
+ coreContext.stop()
+ } else {
+ Log.w("[Side Menu] Keep Service alive setting enabled, don't destroy the Core")
+ }
}
onBackPressedCallback.isEnabled = false
diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/StatusViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/StatusViewModel.kt
index a6d0404a7..27f0afb14 100644
--- a/app/src/main/java/org/linphone/activities/main/viewmodels/StatusViewModel.kt
+++ b/app/src/main/java/org/linphone/activities/main/viewmodels/StatusViewModel.kt
@@ -57,7 +57,7 @@ open class StatusViewModel : ViewModel() {
body: Content
) {
if (body.type == "application" && body.subtype == "simple-message-summary" && body.size > 0) {
- val data = body.utf8Text?.toLowerCase(Locale.getDefault())
+ val data = body.utf8Text?.lowercase(Locale.getDefault())
val voiceMail = data?.split("voice-message: ")
if (voiceMail?.size ?: 0 >= 2) {
val toParse = voiceMail!![1].split("/", limit = 0)
diff --git a/app/src/main/java/org/linphone/compatibility/Api21Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api21Compatibility.kt
index cd59c2076..db1a1b764 100644
--- a/app/src/main/java/org/linphone/compatibility/Api21Compatibility.kt
+++ b/app/src/main/java/org/linphone/compatibility/Api21Compatibility.kt
@@ -22,9 +22,11 @@ package org.linphone.compatibility
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Activity
+import android.app.PendingIntent
import android.bluetooth.BluetoothAdapter
import android.content.ContentValues
import android.content.Context
+import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
@@ -33,6 +35,7 @@ import android.os.Vibrator
import android.provider.MediaStore
import android.provider.Settings
import android.view.WindowManager
+import android.view.inputmethod.EditorInfo
import org.linphone.R
import org.linphone.core.Content
import org.linphone.core.tools.Log
@@ -44,8 +47,10 @@ import org.linphone.utils.PermissionHelper
@TargetApi(21)
class Api21Compatibility {
companion object {
+ @SuppressLint("MissingPermission")
fun getDeviceName(context: Context): String {
- var name = BluetoothAdapter.getDefaultAdapter().name
+ val adapter = BluetoothAdapter.getDefaultAdapter()
+ var name = adapter?.name
if (name == null) {
name = Settings.Secure.getString(
context.contentResolver,
@@ -74,7 +79,10 @@ class Api21Compatibility {
return false
}
- val filePath = content.filePath
+ val plainFilePath = content.plainFilePath.orEmpty()
+ val isVfsEncrypted = plainFilePath.isNotEmpty()
+ Log.w("[Media Store] Content is encrypted, requesting plain file path")
+ val filePath = if (isVfsEncrypted) plainFilePath else content.filePath
if (filePath == null) {
Log.e("[Media Store] Content doesn't have a file path!")
return false
@@ -92,6 +100,10 @@ class Api21Compatibility {
}
val collection = MediaStore.Images.Media.getContentUri("external")
val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values)
+ if (isVfsEncrypted) {
+ Log.w("[Media Store] Content was encrypted, delete plain version: $plainFilePath")
+ FileUtils.deleteFile(plainFilePath)
+ }
if (mediaStoreFilePath.isNotEmpty()) {
content.userData = mediaStoreFilePath
return true
@@ -105,7 +117,10 @@ class Api21Compatibility {
return false
}
- val filePath = content.filePath
+ val plainFilePath = content.plainFilePath.orEmpty()
+ val isVfsEncrypted = plainFilePath.isNotEmpty()
+ Log.w("[Media Store] Content is encrypted, requesting plain file path")
+ val filePath = if (isVfsEncrypted) plainFilePath else content.filePath
if (filePath == null) {
Log.e("[Media Store] Content doesn't have a file path!")
return false
@@ -124,6 +139,10 @@ class Api21Compatibility {
}
val collection = MediaStore.Video.Media.getContentUri("external")
val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values)
+ if (isVfsEncrypted) {
+ Log.w("[Media Store] Content was encrypted, delete plain version: $plainFilePath")
+ FileUtils.deleteFile(plainFilePath)
+ }
if (mediaStoreFilePath.isNotEmpty()) {
content.userData = mediaStoreFilePath
return true
@@ -137,7 +156,10 @@ class Api21Compatibility {
return false
}
- val filePath = content.filePath
+ val plainFilePath = content.plainFilePath.orEmpty()
+ val isVfsEncrypted = plainFilePath.isNotEmpty()
+ Log.w("[Media Store] Content is encrypted, requesting plain file path")
+ val filePath = if (isVfsEncrypted) plainFilePath else content.filePath
if (filePath == null) {
Log.e("[Media Store] Content doesn't have a file path!")
return false
@@ -157,6 +179,10 @@ class Api21Compatibility {
val collection = MediaStore.Audio.Media.getContentUri("external")
val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values)
+ if (isVfsEncrypted) {
+ Log.w("[Media Store] Content was encrypted, delete plain version: $plainFilePath")
+ FileUtils.deleteFile(plainFilePath)
+ }
if (mediaStoreFilePath.isNotEmpty()) {
content.userData = mediaStoreFilePath
return true
@@ -207,5 +233,17 @@ class Api21Compatibility {
fun requestDismissKeyguard(activity: Activity) {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD)
}
+
+ fun getUpdateCurrentPendingIntentFlag(): Int {
+ return PendingIntent.FLAG_UPDATE_CURRENT
+ }
+
+ fun getImeFlagsForSecureChatRoom(): Int {
+ return EditorInfo.IME_FLAG_NO_EXTRACT_UI
+ }
+
+ fun startForegroundService(context: Context, intent: Intent) {
+ context.startService(intent)
+ }
}
}
diff --git a/app/src/main/java/org/linphone/compatibility/Api23Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api23Compatibility.kt
index 080d8f739..4a7505564 100644
--- a/app/src/main/java/org/linphone/compatibility/Api23Compatibility.kt
+++ b/app/src/main/java/org/linphone/compatibility/Api23Compatibility.kt
@@ -19,10 +19,12 @@
*/
package org.linphone.compatibility
+import android.Manifest
import android.annotation.TargetApi
import android.content.Context
import android.content.pm.PackageManager
import android.provider.Settings
+import androidx.fragment.app.Fragment
@TargetApi(23)
class Api23Compatibility {
@@ -31,6 +33,10 @@ class Api23Compatibility {
return context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED
}
+ fun requestReadPhoneStatePermission(fragment: Fragment, code: Int) {
+ fragment.requestPermissions(arrayOf(Manifest.permission.READ_PHONE_STATE), code)
+ }
+
fun canDrawOverlay(context: Context): Boolean {
return Settings.canDrawOverlays(context)
}
diff --git a/app/src/main/java/org/linphone/compatibility/Api25Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api25Compatibility.kt
index 084ee9d71..a813f8a81 100644
--- a/app/src/main/java/org/linphone/compatibility/Api25Compatibility.kt
+++ b/app/src/main/java/org/linphone/compatibility/Api25Compatibility.kt
@@ -34,7 +34,8 @@ class Api25Compatibility {
context.contentResolver, Settings.Global.DEVICE_NAME
)
if (name == null) {
- name = BluetoothAdapter.getDefaultAdapter().name
+ val adapter = BluetoothAdapter.getDefaultAdapter()
+ name = adapter?.name
}
if (name == null) {
name = Settings.Secure.getString(
diff --git a/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt
index 8f66015e0..f49e1bdd3 100644
--- a/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt
+++ b/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt
@@ -19,22 +19,33 @@
*/
package org.linphone.compatibility
+import android.Manifest
import android.annotation.SuppressLint
import android.annotation.TargetApi
-import android.app.Activity
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PictureInPictureParams
+import android.app.*
import android.content.Context
+import android.content.Intent
import android.content.pm.PackageManager
import android.media.AudioAttributes
import android.os.VibrationEffect
import android.os.Vibrator
import android.view.WindowManager
+import android.view.inputmethod.EditorInfo
+import android.widget.RemoteViews
+import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
+import androidx.core.content.ContextCompat
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
+import org.linphone.contact.Contact
+import org.linphone.core.Call
import org.linphone.core.tools.Log
+import org.linphone.notifications.Notifiable
+import org.linphone.notifications.NotificationsManager
import org.linphone.telecom.NativeCallWrapper
+import org.linphone.utils.ImageUtils
+import org.linphone.utils.LinphoneUtils
@TargetApi(26)
class Api26Compatibility {
@@ -126,6 +137,117 @@ class Api26Compatibility {
return WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
}
+ fun createIncomingCallNotification(
+ context: Context,
+ call: Call,
+ notifiable: Notifiable,
+ pendingIntent: PendingIntent,
+ notificationsManager: NotificationsManager
+ ): Notification {
+ val contact: Contact? = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
+ val pictureUri = contact?.getContactThumbnailPictureUri()
+ val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri)
+ val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress)
+ val address = LinphoneUtils.getDisplayableAddress(call.remoteAddress)
+
+ val notificationLayoutHeadsUp = RemoteViews(context.packageName, R.layout.call_incoming_notification_heads_up)
+ notificationLayoutHeadsUp.setTextViewText(R.id.caller, displayName)
+ notificationLayoutHeadsUp.setTextViewText(R.id.sip_uri, address)
+ notificationLayoutHeadsUp.setTextViewText(R.id.incoming_call_info, context.getString(R.string.incoming_call_notification_title))
+
+ if (roundPicture != null) {
+ notificationLayoutHeadsUp.setImageViewBitmap(R.id.caller_picture, roundPicture)
+ }
+
+ val builder = NotificationCompat.Builder(context, context.getString(R.string.notification_channel_incoming_call_id))
+ .setStyle(NotificationCompat.DecoratedCustomViewStyle())
+ .addPerson(notificationsManager.getPerson(contact, displayName, roundPicture))
+ .setSmallIcon(R.drawable.topbar_call_notification)
+ .setContentTitle(displayName)
+ .setContentText(context.getString(R.string.incoming_call_notification_title))
+ .setCategory(NotificationCompat.CATEGORY_CALL)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setWhen(System.currentTimeMillis())
+ .setAutoCancel(false)
+ .setShowWhen(true)
+ .setOngoing(true)
+ .setColor(ContextCompat.getColor(context, R.color.primary_color))
+ .setFullScreenIntent(pendingIntent, true)
+ .addAction(notificationsManager.getCallDeclineAction(notifiable))
+ .addAction(notificationsManager.getCallAnswerAction(notifiable))
+ .setCustomHeadsUpContentView(notificationLayoutHeadsUp)
+
+ if (!corePreferences.preventInterfaceFromShowingUp) {
+ builder.setContentIntent(pendingIntent)
+ }
+
+ return builder.build()
+ }
+
+ fun createCallNotification(
+ context: Context,
+ call: Call,
+ notifiable: Notifiable,
+ pendingIntent: PendingIntent,
+ channel: String,
+ notificationsManager: NotificationsManager
+ ): Notification {
+ val contact: Contact? = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
+ val pictureUri = contact?.getContactThumbnailPictureUri()
+ val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri)
+ val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress)
+
+ val stringResourceId: Int
+ val iconResourceId: Int
+ when (call.state) {
+ Call.State.Paused, Call.State.Pausing, Call.State.PausedByRemote -> {
+ stringResourceId = R.string.call_notification_paused
+ iconResourceId = R.drawable.topbar_call_paused_notification
+ }
+ Call.State.OutgoingRinging, Call.State.OutgoingProgress, Call.State.OutgoingInit, Call.State.OutgoingEarlyMedia -> {
+ stringResourceId = R.string.call_notification_outgoing
+ iconResourceId = if (call.params.isVideoEnabled) {
+ R.drawable.topbar_videocall_notification
+ } else {
+ R.drawable.topbar_call_notification
+ }
+ }
+ else -> {
+ stringResourceId = R.string.call_notification_active
+ iconResourceId = if (call.currentParams.isVideoEnabled) {
+ R.drawable.topbar_videocall_notification
+ } else {
+ R.drawable.topbar_call_notification
+ }
+ }
+ }
+
+ val builder = NotificationCompat.Builder(
+ context, channel
+ )
+ .setContentTitle(contact?.fullName ?: displayName)
+ .setContentText(context.getString(stringResourceId))
+ .setSmallIcon(iconResourceId)
+ .setLargeIcon(roundPicture)
+ .addPerson(notificationsManager.getPerson(contact, displayName, roundPicture))
+ .setAutoCancel(false)
+ .setCategory(NotificationCompat.CATEGORY_CALL)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setWhen(System.currentTimeMillis())
+ .setShowWhen(true)
+ .setOngoing(true)
+ .setColor(ContextCompat.getColor(context, R.color.notification_led_color))
+ .addAction(notificationsManager.getCallDeclineAction(notifiable))
+
+ if (!corePreferences.preventInterfaceFromShowingUp) {
+ builder.setContentIntent(pendingIntent)
+ }
+
+ return builder.build()
+ }
+
@SuppressLint("MissingPermission")
fun eventVibration(vibrator: Vibrator) {
val effect = VibrationEffect.createWaveform(longArrayOf(0L, 100L, 100L), intArrayOf(0, VibrationEffect.DEFAULT_AMPLITUDE, 0), -1)
@@ -135,8 +257,37 @@ class Api26Compatibility {
vibrator.vibrate(effect, audioAttrs)
}
- fun changeAudioRouteForTelecomManager(connection: NativeCallWrapper, route: Int) {
+ fun changeAudioRouteForTelecomManager(connection: NativeCallWrapper, route: Int): Boolean {
+ Log.i("[Telecom Helper] Changing audio route [$route] on connection ${connection.callId}")
+ if (connection.callAudioState.route == route) {
+ Log.w("[Telecom Helper] Connection is already using this route")
+ return false
+ }
connection.setAudioRoute(route)
+ return true
+ }
+
+ fun requestTelecomManagerPermission(activity: Activity, code: Int) {
+ activity.requestPermissions(
+ arrayOf(
+ Manifest.permission.READ_PHONE_STATE,
+ Manifest.permission.MANAGE_OWN_CALLS
+ ),
+ code
+ )
+ }
+
+ fun hasTelecomManagerPermission(context: Context): Boolean {
+ return Compatibility.hasPermission(context, Manifest.permission.READ_PHONE_STATE) &&
+ Compatibility.hasPermission(context, Manifest.permission.MANAGE_OWN_CALLS)
+ }
+
+ fun getImeFlagsForSecureChatRoom(): Int {
+ return EditorInfo.IME_FLAG_NO_EXTRACT_UI or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING
+ }
+
+ fun startForegroundService(context: Context, intent: Intent) {
+ context.startForegroundService(intent)
}
}
}
diff --git a/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt
index 9ffd1e3ea..9bb62eedc 100644
--- a/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt
+++ b/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt
@@ -21,7 +21,6 @@ package org.linphone.compatibility
import android.Manifest
import android.annotation.TargetApi
-import android.app.Activity
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.ContentValues
@@ -57,10 +56,6 @@ class Api29Compatibility {
return granted
}
- fun requestReadPhoneStatePermission(activity: Activity, code: Int) {
- activity.requestPermissions(arrayOf(Manifest.permission.READ_PHONE_STATE), code)
- }
-
fun createMessageChannel(
context: Context,
notificationManager: NotificationManagerCompat
@@ -104,7 +99,10 @@ class Api29Compatibility {
}
suspend fun addImageToMediaStore(context: Context, content: Content): Boolean {
- val filePath = content.filePath
+ val plainFilePath = content.plainFilePath.orEmpty()
+ val isVfsEncrypted = plainFilePath.isNotEmpty()
+ Log.w("[Media Store] Content is encrypted, requesting plain file path")
+ val filePath = if (isVfsEncrypted) plainFilePath else content.filePath
if (filePath == null) {
Log.e("[Media Store] Content doesn't have a file path!")
return false
@@ -124,6 +122,11 @@ class Api29Compatibility {
}
val collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values, MediaStore.Images.Media.IS_PENDING)
+
+ if (isVfsEncrypted) {
+ Log.w("[Media Store] Content was encrypted, delete plain version: $plainFilePath")
+ FileUtils.deleteFile(plainFilePath)
+ }
if (mediaStoreFilePath.isNotEmpty()) {
content.userData = mediaStoreFilePath
return true
@@ -132,7 +135,10 @@ class Api29Compatibility {
}
suspend fun addVideoToMediaStore(context: Context, content: Content): Boolean {
- val filePath = content.filePath
+ val plainFilePath = content.plainFilePath.orEmpty()
+ val isVfsEncrypted = plainFilePath.isNotEmpty()
+ Log.w("[Media Store] Content is encrypted, requesting plain file path")
+ val filePath = if (isVfsEncrypted) plainFilePath else content.filePath
if (filePath == null) {
Log.e("[Media Store] Content doesn't have a file path!")
return false
@@ -153,6 +159,11 @@ class Api29Compatibility {
}
val collection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values, MediaStore.Video.Media.IS_PENDING)
+
+ if (isVfsEncrypted) {
+ Log.w("[Media Store] Content was encrypted, delete plain version: $plainFilePath")
+ FileUtils.deleteFile(plainFilePath)
+ }
if (mediaStoreFilePath.isNotEmpty()) {
content.userData = mediaStoreFilePath
return true
@@ -161,7 +172,10 @@ class Api29Compatibility {
}
suspend fun addAudioToMediaStore(context: Context, content: Content): Boolean {
- val filePath = content.filePath
+ val plainFilePath = content.plainFilePath.orEmpty()
+ val isVfsEncrypted = plainFilePath.isNotEmpty()
+ Log.w("[Media Store] Content is encrypted, requesting plain file path")
+ val filePath = if (isVfsEncrypted) plainFilePath else content.filePath
if (filePath == null) {
Log.e("[Media Store] Content doesn't have a file path!")
return false
@@ -183,6 +197,11 @@ class Api29Compatibility {
val collection = MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val mediaStoreFilePath = addContentValuesToCollection(context, filePath, collection, values, MediaStore.Audio.Media.IS_PENDING)
+
+ if (isVfsEncrypted) {
+ Log.w("[Media Store] Content was encrypted, delete plain version: $plainFilePath")
+ FileUtils.deleteFile(plainFilePath)
+ }
if (mediaStoreFilePath.isNotEmpty()) {
content.userData = mediaStoreFilePath
return true
diff --git a/app/src/main/java/org/linphone/compatibility/Api30Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api30Compatibility.kt
index dcf7d050b..70637bbd2 100644
--- a/app/src/main/java/org/linphone/compatibility/Api30Compatibility.kt
+++ b/app/src/main/java/org/linphone/compatibility/Api30Compatibility.kt
@@ -24,6 +24,7 @@ import android.annotation.TargetApi
import android.app.Activity
import android.content.Context
import android.content.pm.ShortcutManager
+import androidx.fragment.app.Fragment
import org.linphone.core.ChatRoom
import org.linphone.core.tools.Log
import org.linphone.utils.LinphoneUtils
@@ -41,8 +42,25 @@ class Api30Compatibility {
return granted
}
- fun requestReadPhoneNumbersPermission(activity: Activity, code: Int) {
- activity.requestPermissions(arrayOf(Manifest.permission.READ_PHONE_NUMBERS), code)
+ fun requestReadPhoneNumbersPermission(fragment: Fragment, code: Int) {
+ fragment.requestPermissions(arrayOf(Manifest.permission.READ_PHONE_NUMBERS), code)
+ }
+
+ fun requestTelecomManagerPermission(activity: Activity, code: Int) {
+ activity.requestPermissions(
+ arrayOf(
+ Manifest.permission.READ_PHONE_NUMBERS,
+ Manifest.permission.READ_PHONE_STATE,
+ Manifest.permission.MANAGE_OWN_CALLS
+ ),
+ code
+ )
+ }
+
+ fun hasTelecomManagerPermission(context: Context): Boolean {
+ return Compatibility.hasPermission(context, Manifest.permission.READ_PHONE_NUMBERS) &&
+ Compatibility.hasPermission(context, Manifest.permission.READ_PHONE_STATE) &&
+ Compatibility.hasPermission(context, Manifest.permission.MANAGE_OWN_CALLS)
}
fun removeChatRoomShortcut(context: Context, chatRoom: ChatRoom) {
diff --git a/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt
new file mode 100644
index 000000000..82262693e
--- /dev/null
+++ b/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.compatibility
+
+import android.annotation.TargetApi
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Person
+import android.content.Context
+import androidx.core.content.ContextCompat
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.LinphoneApplication.Companion.corePreferences
+import org.linphone.R
+import org.linphone.contact.Contact
+import org.linphone.core.Call
+import org.linphone.notifications.Notifiable
+import org.linphone.notifications.NotificationsManager
+import org.linphone.utils.ImageUtils
+import org.linphone.utils.LinphoneUtils
+
+@TargetApi(31)
+class Api31Compatibility {
+ companion object {
+ fun getUpdateCurrentPendingIntentFlag(): Int {
+ return PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+ }
+
+ fun createIncomingCallNotification(
+ context: Context,
+ call: Call,
+ notifiable: Notifiable,
+ pendingIntent: PendingIntent,
+ notificationsManager: NotificationsManager
+ ): Notification {
+ val contact: Contact? = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
+ val pictureUri = contact?.getContactThumbnailPictureUri()
+ val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri)
+ val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress)
+
+ val person = notificationsManager.getPerson(contact, displayName, roundPicture)
+ val caller = Person.Builder()
+ .setName(person.name)
+ .setIcon(person.icon?.toIcon(context))
+ .setUri(person.uri)
+ .setKey(person.key)
+ .setImportant(person.isImportant)
+ .build()
+ val declineIntent = notificationsManager.getCallDeclinePendingIntent(notifiable)
+ val answerIntent = notificationsManager.getCallAnswerPendingIntent(notifiable)
+ val builder = Notification.Builder(context, context.getString(R.string.notification_channel_incoming_call_id))
+ .setStyle(Notification.CallStyle.forIncomingCall(caller, declineIntent, answerIntent))
+ .setSmallIcon(R.drawable.topbar_call_notification)
+ .setCategory(Notification.CATEGORY_CALL)
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
+ .setWhen(System.currentTimeMillis())
+ .setAutoCancel(false)
+ .setShowWhen(true)
+ .setOngoing(true)
+ .setColor(ContextCompat.getColor(context, R.color.primary_color))
+ .setFullScreenIntent(pendingIntent, true)
+
+ if (!corePreferences.preventInterfaceFromShowingUp) {
+ builder.setContentIntent(pendingIntent)
+ }
+
+ return builder.build()
+ }
+
+ fun createCallNotification(
+ context: Context,
+ call: Call,
+ notifiable: Notifiable,
+ pendingIntent: PendingIntent,
+ channel: String,
+ notificationsManager: NotificationsManager
+ ): Notification {
+ val contact: Contact? = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
+ val pictureUri = contact?.getContactThumbnailPictureUri()
+ val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri)
+ val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress)
+
+ val isVideo = call.currentParams.isVideoEnabled
+ val iconResourceId: Int = when (call.state) {
+ Call.State.Paused, Call.State.Pausing, Call.State.PausedByRemote -> {
+ R.drawable.topbar_call_paused_notification
+ }
+ else -> {
+ if (isVideo) {
+ R.drawable.topbar_videocall_notification
+ } else {
+ R.drawable.topbar_call_notification
+ }
+ }
+ }
+
+ val person = notificationsManager.getPerson(contact, displayName, roundPicture)
+ val caller = Person.Builder()
+ .setName(person.name)
+ .setIcon(person.icon?.toIcon(context))
+ .setUri(person.uri)
+ .setKey(person.key)
+ .setImportant(person.isImportant)
+ .build()
+ val declineIntent = notificationsManager.getCallDeclinePendingIntent(notifiable)
+ val builder = Notification.Builder(
+ context, channel
+ )
+ .setStyle(Notification.CallStyle.forOngoingCall(caller, declineIntent).setIsVideo(isVideo))
+ .setSmallIcon(iconResourceId)
+ .setAutoCancel(false)
+ .setCategory(Notification.CATEGORY_CALL)
+ .setVisibility(Notification.VISIBILITY_PUBLIC)
+ .setWhen(System.currentTimeMillis())
+ .setShowWhen(true)
+ .setOngoing(true)
+ .setColor(ContextCompat.getColor(context, R.color.notification_led_color))
+ .setFullScreenIntent(pendingIntent, true) // This is required for CallStyle notification
+
+ if (!corePreferences.preventInterfaceFromShowingUp) {
+ builder.setContentIntent(pendingIntent)
+ }
+
+ return builder.build()
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/compatibility/Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Compatibility.kt
index c1c23718a..e01d09c95 100644
--- a/app/src/main/java/org/linphone/compatibility/Compatibility.kt
+++ b/app/src/main/java/org/linphone/compatibility/Compatibility.kt
@@ -20,19 +20,27 @@
package org.linphone.compatibility
import android.app.Activity
+import android.app.Notification
+import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
+import android.os.Build
import android.os.Vibrator
import android.telephony.TelephonyManager
import android.view.View
import android.view.WindowManager
import androidx.core.app.NotificationManagerCompat
+import androidx.fragment.app.Fragment
+import java.util.*
+import org.linphone.core.Call
import org.linphone.core.ChatRoom
import org.linphone.core.Content
import org.linphone.mediastream.Version
+import org.linphone.notifications.Notifiable
+import org.linphone.notifications.NotificationsManager
import org.linphone.telecom.NativeCallWrapper
@Suppress("DEPRECATION")
@@ -55,11 +63,28 @@ class Compatibility {
}
// See https://developer.android.com/about/versions/11/privacy/permissions#phone-numbers
- fun requestReadPhoneStateOrNumbersPermission(activity: Activity, code: Int) {
+ fun requestReadPhoneStateOrNumbersPermission(fragment: Fragment, code: Int) {
if (Version.sdkAboveOrEqual(Version.API30_ANDROID_11)) {
- Api30Compatibility.requestReadPhoneNumbersPermission(activity, code)
+ Api30Compatibility.requestReadPhoneNumbersPermission(fragment, code)
} else {
- Api29Compatibility.requestReadPhoneStatePermission(activity, code)
+ Api23Compatibility.requestReadPhoneStatePermission(fragment, code)
+ }
+ }
+
+ // See https://developer.android.com/about/versions/11/privacy/permissions#phone-numbers
+ fun hasTelecomManagerPermissions(context: Context): Boolean {
+ return if (Version.sdkAboveOrEqual(Version.API30_ANDROID_11)) {
+ Api30Compatibility.hasTelecomManagerPermission(context)
+ } else {
+ Api26Compatibility.hasTelecomManagerPermission(context)
+ }
+ }
+
+ fun requestTelecomManagerPermissions(activity: Activity, code: Int) {
+ if (Version.sdkAboveOrEqual(Version.API30_ANDROID_11)) {
+ Api30Compatibility.requestTelecomManagerPermission(activity, code)
+ } else {
+ Api26Compatibility.requestTelecomManagerPermission(activity, code)
}
}
@@ -147,6 +172,43 @@ class Compatibility {
return WindowManager.LayoutParams.TYPE_PHONE
}
+ fun createIncomingCallNotification(
+ context: Context,
+ call: Call,
+ notifiable: Notifiable,
+ pendingIntent: PendingIntent,
+ notificationsManager: NotificationsManager
+ ): Notification {
+ if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
+ return Api31Compatibility.createIncomingCallNotification(context, call, notifiable, pendingIntent, notificationsManager)
+ } else if (Build.MANUFACTURER.lowercase(Locale.getDefault()) == "xiaomi") {
+ return XiaomiCompatibility.createIncomingCallNotification(context, call, notifiable, pendingIntent, notificationsManager)
+ }
+ return Api26Compatibility.createIncomingCallNotification(context, call, notifiable, pendingIntent, notificationsManager)
+ }
+
+ fun createCallNotification(
+ context: Context,
+ call: Call,
+ notifiable: Notifiable,
+ pendingIntent: PendingIntent,
+ channel: String,
+ notificationsManager: NotificationsManager
+ ): Notification {
+ if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
+ return Api31Compatibility.createCallNotification(context, call, notifiable, pendingIntent, channel, notificationsManager)
+ }
+ return Api26Compatibility.createCallNotification(context, call, notifiable, pendingIntent, channel, notificationsManager)
+ }
+
+ fun startForegroundService(context: Context, intent: Intent) {
+ if (Version.sdkAboveOrEqual(Version.API26_O_80)) {
+ Api26Compatibility.startForegroundService(context, intent)
+ } else {
+ Api21Compatibility.startForegroundService(context, intent)
+ }
+ }
+
/* Call */
fun canDrawOverlay(context: Context): Boolean {
@@ -170,10 +232,11 @@ class Compatibility {
}
}
- fun changeAudioRouteForTelecomManager(connection: NativeCallWrapper, route: Int) {
+ fun changeAudioRouteForTelecomManager(connection: NativeCallWrapper, route: Int): Boolean {
if (Version.sdkAboveOrEqual(Version.API26_O_80)) {
- Api26Compatibility.changeAudioRouteForTelecomManager(connection, route)
+ return Api26Compatibility.changeAudioRouteForTelecomManager(connection, route)
}
+ return false
}
/* Contacts */
@@ -244,5 +307,19 @@ class Compatibility {
}
return Api21Compatibility.addAudioToMediaStore(context, content)
}
+
+ fun getUpdateCurrentPendingIntentFlag(): Int {
+ if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
+ return Api31Compatibility.getUpdateCurrentPendingIntentFlag()
+ }
+ return Api21Compatibility.getUpdateCurrentPendingIntentFlag()
+ }
+
+ fun getImeFlagsForSecureChatRoom(): Int {
+ if (Version.sdkAboveOrEqual(Version.API26_O_80)) {
+ return Api26Compatibility.getImeFlagsForSecureChatRoom()
+ }
+ return Api21Compatibility.getImeFlagsForSecureChatRoom()
+ }
}
}
diff --git a/app/src/main/java/org/linphone/compatibility/TelephonyListener.kt b/app/src/main/java/org/linphone/compatibility/TelephonyListener.kt
index b946cc94e..df96c50f6 100644
--- a/app/src/main/java/org/linphone/compatibility/TelephonyListener.kt
+++ b/app/src/main/java/org/linphone/compatibility/TelephonyListener.kt
@@ -33,7 +33,7 @@ class TelephonyListener(private val telephonyManager: TelephonyManager) : PhoneS
private fun runOnUiThreadExecutor(): Executor {
val handler = Handler(Looper.getMainLooper())
- return Executor() {
+ return Executor {
handler.post(it)
}
}
diff --git a/app/src/main/java/org/linphone/compatibility/XiaomiCompatibility.kt b/app/src/main/java/org/linphone/compatibility/XiaomiCompatibility.kt
new file mode 100644
index 000000000..05b6a482c
--- /dev/null
+++ b/app/src/main/java/org/linphone/compatibility/XiaomiCompatibility.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.compatibility
+
+import android.annotation.TargetApi
+import android.app.*
+import android.content.Context
+import android.graphics.BitmapFactory
+import androidx.core.app.NotificationCompat
+import androidx.core.content.ContextCompat
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.LinphoneApplication.Companion.corePreferences
+import org.linphone.R
+import org.linphone.contact.Contact
+import org.linphone.core.Call
+import org.linphone.notifications.Notifiable
+import org.linphone.notifications.NotificationsManager
+import org.linphone.utils.ImageUtils
+import org.linphone.utils.LinphoneUtils
+
+@TargetApi(26)
+class XiaomiCompatibility {
+ companion object {
+ fun createIncomingCallNotification(
+ context: Context,
+ call: Call,
+ notifiable: Notifiable,
+ pendingIntent: PendingIntent,
+ notificationsManager: NotificationsManager
+ ): Notification {
+ val contact: Contact? = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
+ val pictureUri = contact?.getContactThumbnailPictureUri()
+ val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri)
+ val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress)
+ val address = LinphoneUtils.getDisplayableAddress(call.remoteAddress)
+
+ val builder = NotificationCompat.Builder(context, context.getString(R.string.notification_channel_incoming_call_id))
+ .addPerson(notificationsManager.getPerson(contact, displayName, roundPicture))
+ .setSmallIcon(R.drawable.topbar_call_notification)
+ .setLargeIcon(roundPicture ?: BitmapFactory.decodeResource(context.resources, R.drawable.avatar))
+ .setContentTitle(displayName)
+ .setContentText(address)
+ .setSubText(context.getString(R.string.incoming_call_notification_title))
+ .setCategory(NotificationCompat.CATEGORY_CALL)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setWhen(System.currentTimeMillis())
+ .setAutoCancel(false)
+ .setShowWhen(true)
+ .setOngoing(true)
+ .setColor(ContextCompat.getColor(context, R.color.primary_color))
+ .setFullScreenIntent(pendingIntent, true)
+ .addAction(notificationsManager.getCallDeclineAction(notifiable))
+ .addAction(notificationsManager.getCallAnswerAction(notifiable))
+
+ if (!corePreferences.preventInterfaceFromShowingUp) {
+ builder.setContentIntent(pendingIntent)
+ }
+
+ return builder.build()
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/contact/AsyncContactsLoader.kt b/app/src/main/java/org/linphone/contact/AsyncContactsLoader.kt
index 3433f893b..b554d0a40 100644
--- a/app/src/main/java/org/linphone/contact/AsyncContactsLoader.kt
+++ b/app/src/main/java/org/linphone/contact/AsyncContactsLoader.kt
@@ -129,10 +129,10 @@ class AsyncContactsLoader(private val context: Context) :
try {
val id: String =
- cursor.getString(cursor.getColumnIndex(ContactsContract.Data.CONTACT_ID))
+ cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Data.CONTACT_ID))
val starred =
- cursor.getInt(cursor.getColumnIndex(ContactsContract.Contacts.STARRED)) == 1
- val lookupKey = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY))
+ cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.STARRED)) == 1
+ val lookupKey = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.LOOKUP_KEY))
var contact: Contact? = androidContactsCache[id]
if (contact == null) {
Log.d(
@@ -148,6 +148,10 @@ class AsyncContactsLoader(private val context: Context) :
Log.e(
"[Contacts Loader] Couldn't get values from cursor, exception: $ise"
)
+ } catch (iae: IllegalArgumentException) {
+ Log.e(
+ "[Contacts Loader] Couldn't get values from cursor, exception: $iae"
+ )
}
}
cursor.close()
diff --git a/app/src/main/java/org/linphone/contact/Contact.kt b/app/src/main/java/org/linphone/contact/Contact.kt
index 2d370a1eb..8cba22e3a 100644
--- a/app/src/main/java/org/linphone/contact/Contact.kt
+++ b/app/src/main/java/org/linphone/contact/Contact.kt
@@ -51,10 +51,10 @@ open class Contact : Comparable {
// Raw SIP addresses are only used for contact edition
var rawSipAddresses = arrayListOf()
- var thumbnailUri: Uri? = null
-
var friend: Friend? = null
+ private var thumbnailUri: Uri? = null
+
override fun compareTo(other: Contact): Int {
val fn = fullName ?: ""
val otherFn = other.fullName ?: ""
@@ -151,7 +151,7 @@ open class Contact : Comparable {
if (bm == null) IconCompat.createWithResource(
coreContext.context,
R.drawable.avatar
- ) else IconCompat.createWithBitmap(bm)
+ ) else IconCompat.createWithAdaptiveBitmap(bm)
if (icon != null) {
personBuilder.setIcon(icon)
}
diff --git a/app/src/main/java/org/linphone/contact/ContactsManager.kt b/app/src/main/java/org/linphone/contact/ContactsManager.kt
index e5c28ef6e..fdc2af0d8 100644
--- a/app/src/main/java/org/linphone/contact/ContactsManager.kt
+++ b/app/src/main/java/org/linphone/contact/ContactsManager.kt
@@ -79,12 +79,6 @@ class ContactsManager(private val context: Context) {
@Synchronized
private set
- var localAccountsContacts = ArrayList()
- @Synchronized
- get
- @Synchronized
- private set
-
val magicSearch: MagicSearch by lazy {
val magicSearch = coreContext.core.createMagicSearch()
magicSearch.limitedSearch = false
@@ -93,6 +87,12 @@ class ContactsManager(private val context: Context) {
var latestContactFetch: String = ""
+ private var localAccountsContacts = ArrayList()
+ @Synchronized
+ get
+ @Synchronized
+ private set
+
private val friendsMap: HashMap = HashMap()
private val contactsUpdatedListeners = ArrayList()
diff --git a/app/src/main/java/org/linphone/contact/DummyAuthenticationService.kt b/app/src/main/java/org/linphone/contact/DummyAuthenticationService.kt
index 3cc3cf4b0..001568cab 100644
--- a/app/src/main/java/org/linphone/contact/DummyAuthenticationService.kt
+++ b/app/src/main/java/org/linphone/contact/DummyAuthenticationService.kt
@@ -28,6 +28,7 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
+import org.linphone.core.tools.Log
// Below classes are required to be able to create our DummySyncService...
internal class DummyAuthenticator(context: Context) : AbstractAccountAuthenticator(context) {
@@ -88,6 +89,7 @@ class DummyAuthenticationService : Service() {
override fun onCreate() {
authenticator = DummyAuthenticator(this)
+ Log.i("[Dummy Auth Service] Authenticator created")
}
override fun onBind(intent: Intent): IBinder {
diff --git a/app/src/main/java/org/linphone/contact/DummySyncService.kt b/app/src/main/java/org/linphone/contact/DummySyncService.kt
index 60cec9642..a040dafca 100644
--- a/app/src/main/java/org/linphone/contact/DummySyncService.kt
+++ b/app/src/main/java/org/linphone/contact/DummySyncService.kt
@@ -24,6 +24,7 @@ import android.app.Service
import android.content.*
import android.os.Bundle
import android.os.IBinder
+import org.linphone.core.tools.Log
// Below classes are required to be able to use our own contact MIME type entry...
class DummySyncAdapter(context: Context, autoInit: Boolean) : AbstractThreadedSyncAdapter(context, autoInit) {
@@ -44,8 +45,10 @@ class DummySyncService : Service() {
override fun onCreate() {
synchronized(syncAdapterLock) {
+ Log.i("[Dummy Sync Adapter] Sync Service created")
if (syncAdapter == null) {
syncAdapter = DummySyncAdapter(applicationContext, true)
+ Log.i("[Dummy Sync Adapter] Sync Adapter created")
}
}
}
diff --git a/app/src/main/java/org/linphone/contact/NativeContact.kt b/app/src/main/java/org/linphone/contact/NativeContact.kt
index b2a109227..c28f6fd6b 100644
--- a/app/src/main/java/org/linphone/contact/NativeContact.kt
+++ b/app/src/main/java/org/linphone/contact/NativeContact.kt
@@ -71,7 +71,7 @@ class NativeContact(val nativeId: String, private val lookupKey: String? = null)
if (bm == null) IconCompat.createWithResource(
coreContext.context,
R.drawable.avatar
- ) else IconCompat.createWithBitmap(bm)
+ ) else IconCompat.createWithAdaptiveBitmap(bm)
if (icon != null) {
personBuilder.setIcon(icon)
}
@@ -86,91 +86,104 @@ class NativeContact(val nativeId: String, private val lookupKey: String? = null)
@Synchronized
override fun syncValuesFromAndroidCursor(cursor: Cursor) {
- val displayName: String? =
- cursor.getString(cursor.getColumnIndex(ContactsContract.Data.DISPLAY_NAME_PRIMARY))
+ try {
+ val displayName: String? =
+ cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Data.DISPLAY_NAME_PRIMARY))
- val mime: String? = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.MIMETYPE))
- val data1: String? = cursor.getString(cursor.getColumnIndex("data1"))
- val data2: String? = cursor.getString(cursor.getColumnIndex("data2"))
- val data3: String? = cursor.getString(cursor.getColumnIndex("data3"))
- val data4: String? = cursor.getString(cursor.getColumnIndex("data4"))
+ val mime: String? =
+ cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Data.MIMETYPE))
+ val data1: String? = cursor.getString(cursor.getColumnIndexOrThrow("data1"))
+ val data2: String? = cursor.getString(cursor.getColumnIndexOrThrow("data2"))
+ val data3: String? = cursor.getString(cursor.getColumnIndexOrThrow("data3"))
+ val data4: String? = cursor.getString(cursor.getColumnIndexOrThrow("data4"))
- if (fullName == null || fullName != displayName) {
- Log.d("[Native Contact] Setting display name $displayName")
- fullName = displayName
- }
+ if (fullName == null || fullName != displayName) {
+ Log.d("[Native Contact] Setting display name $displayName")
+ fullName = displayName
+ }
- val linphoneMime = AppUtils.getString(R.string.linphone_address_mime_type)
- when (mime) {
- ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> {
- if (data1 == null && data4 == null) {
- Log.d("[Native Contact] Phone number data is empty")
- return
- }
+ val linphoneMime = AppUtils.getString(R.string.linphone_address_mime_type)
+ when (mime) {
+ ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> {
+ if (data1 == null && data4 == null) {
+ Log.d("[Native Contact] Phone number data is empty")
+ return
+ }
- val labelColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.LABEL)
- val label: String? = cursor.getString(labelColumnIndex)
- val typeColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.TYPE)
- val type: Int = cursor.getInt(typeColumnIndex)
- val typeLabel = ContactsContract.CommonDataKinds.Phone.getTypeLabel(
- coreContext.context.resources,
- type,
- label
- ).toString()
+ val labelColumnIndex =
+ cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL)
+ val label: String? = cursor.getString(labelColumnIndex)
+ val typeColumnIndex =
+ cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.TYPE)
+ val type: Int = cursor.getInt(typeColumnIndex)
+ val typeLabel = ContactsContract.CommonDataKinds.Phone.getTypeLabel(
+ coreContext.context.resources,
+ type,
+ label
+ ).toString()
- val number = data4 ?: data1
- if (number != null && number.isNotEmpty()) {
- Log.d("[Native Contact] Found phone number $data1 ($data4), type label is $typeLabel")
- if (!rawPhoneNumbers.contains(number)) {
- phoneNumbers.add(PhoneNumber(number, typeLabel))
- rawPhoneNumbers.add(number)
+ // data4 = ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER
+ // data1 = ContactsContract.CommonDataKinds.Phone.NUMBER
+ val number = if (corePreferences.preferNormalizedPhoneNumbersFromAddressBook) {
+ data4 ?: data1
+ } else {
+ data1 ?: data4
+ }
+ if (number != null && number.isNotEmpty()) {
+ Log.d("[Native Contact] Found phone number $data1 ($data4), type label is $typeLabel")
+ if (!rawPhoneNumbers.contains(number)) {
+ phoneNumbers.add(PhoneNumber(number, typeLabel))
+ rawPhoneNumbers.add(number)
+ }
}
}
- }
- linphoneMime, ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE -> {
- if (data1 == null) {
- Log.d("[Native Contact] SIP address is null")
- return
- }
+ linphoneMime, ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE -> {
+ if (data1 == null) {
+ Log.d("[Native Contact] SIP address is null")
+ return
+ }
- Log.d("[Native Contact] Found SIP address $data1")
- if (rawPhoneNumbers.contains(data1)) {
- Log.d("[Native Contact] SIP address value already exists in phone numbers list, skipping")
- return
- }
+ Log.d("[Native Contact] Found SIP address $data1")
+ if (rawPhoneNumbers.contains(data1)) {
+ Log.d("[Native Contact] SIP address value already exists in phone numbers list, skipping")
+ return
+ }
- val address: Address? = coreContext.core.interpretUrl(data1)
- if (address == null) {
- Log.e("[Native Contact] Couldn't parse address $data1 !")
- return
- }
+ val address: Address? = coreContext.core.interpretUrl(data1)
+ if (address == null) {
+ Log.e("[Native Contact] Couldn't parse address $data1 !")
+ return
+ }
- val stringAddress = address.asStringUriOnly()
- Log.d("[Native Contact] Found SIP address $stringAddress")
- if (!rawSipAddresses.contains(data1)) {
- sipAddresses.add(address)
- rawSipAddresses.add(data1)
+ val stringAddress = address.asStringUriOnly()
+ Log.d("[Native Contact] Found SIP address $stringAddress")
+ if (!rawSipAddresses.contains(data1)) {
+ sipAddresses.add(address)
+ rawSipAddresses.add(data1)
+ }
+ }
+ ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE -> {
+ if (data1 == null) {
+ Log.d("[Native Contact] Organization is null")
+ return
+ }
+
+ Log.d("[Native Contact] Found organization $data1")
+ organization = data1
+ }
+ ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
+ if (data2 == null && data3 == null) {
+ Log.d("[Native Contact] First name and last name are both null")
+ return
+ }
+
+ Log.d("[Native Contact] Found first name $data2 and last name $data3")
+ firstName = data2
+ lastName = data3
}
}
- ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE -> {
- if (data1 == null) {
- Log.d("[Native Contact] Organization is null")
- return
- }
-
- Log.d("[Native Contact] Found organization $data1")
- organization = data1
- }
- ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> {
- if (data2 == null && data3 == null) {
- Log.d("[Native Contact] First name and last name are both null")
- return
- }
-
- Log.d("[Native Contact] Found first name $data2 and last name $data3")
- firstName = data2
- lastName = data3
- }
+ } catch (iae: IllegalArgumentException) {
+ Log.e("[Native Contact] Exception: $iae")
}
}
@@ -179,7 +192,7 @@ class NativeContact(val nativeId: String, private val lookupKey: String? = null)
var created = false
if (friend == null) {
val friend = coreContext.core.createFriend()
- friend.enableSubscribes(false)
+ friend.isSubscribesEnabled = false
friend.incSubscribePolicy = SubscribePolicy.SPDeny
friend.refKey = nativeId
friend.userData = this
diff --git a/app/src/main/java/org/linphone/contact/NativeContactEditor.kt b/app/src/main/java/org/linphone/contact/NativeContactEditor.kt
index 84f900e74..630cb9703 100644
--- a/app/src/main/java/org/linphone/contact/NativeContactEditor.kt
+++ b/app/src/main/java/org/linphone/contact/NativeContactEditor.kt
@@ -96,8 +96,12 @@ class NativeContactEditor(val contact: NativeContact) {
if (cursor?.moveToFirst() == true) {
do {
if (rawId == null) {
- rawId = cursor.getString(cursor.getColumnIndex(RawContacts._ID))
- Log.i("[Native Contact Editor] Found raw id $rawId for native contact with id ${contact.nativeId}")
+ try {
+ rawId = cursor.getString(cursor.getColumnIndexOrThrow(RawContacts._ID))
+ Log.i("[Native Contact Editor] Found raw id $rawId for native contact with id ${contact.nativeId}")
+ } catch (iae: IllegalArgumentException) {
+ Log.e("[Native Contact Editor] Exception: $iae")
+ }
}
} while (cursor.moveToNext() && rawId == null)
}
@@ -258,11 +262,16 @@ class NativeContactEditor(val contact: NativeContact) {
)
if (cursor?.moveToFirst() == true) {
do {
- val accountType =
- cursor.getString(cursor.getColumnIndex(RawContacts.ACCOUNT_TYPE))
- if (accountType == AppUtils.getString(R.string.sync_account_type) && syncAccountRawId == null) {
- syncAccountRawId = cursor.getString(cursor.getColumnIndex(RawContacts._ID))
- Log.d("[Native Contact Editor] Found linphone raw id $syncAccountRawId for native contact with id ${contact.nativeId}")
+ try {
+ val accountType =
+ cursor.getString(cursor.getColumnIndexOrThrow(RawContacts.ACCOUNT_TYPE))
+ if (accountType == AppUtils.getString(R.string.sync_account_type) && syncAccountRawId == null) {
+ syncAccountRawId =
+ cursor.getString(cursor.getColumnIndexOrThrow(RawContacts._ID))
+ Log.d("[Native Contact Editor] Found linphone raw id $syncAccountRawId for native contact with id ${contact.nativeId}")
+ }
+ } catch (iae: IllegalArgumentException) {
+ Log.e("[Native Contact Editor] Exception: $iae")
}
} while (cursor.moveToNext() && syncAccountRawId == null)
}
@@ -461,7 +470,11 @@ class NativeContactEditor(val contact: NativeContact) {
val count = cursor?.count ?: 0
val data1 = if (count > 0) {
if (cursor?.moveToFirst() == true) {
- cursor.getString(cursor.getColumnIndex("data1"))
+ try {
+ cursor.getString(cursor.getColumnIndexOrThrow("data1"))
+ } catch (iae: IllegalArgumentException) {
+ Log.e("[Native Contact Editor] Exception: $iae")
+ }
} else null
} else null
cursor?.close()
diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt
index bb2197f9a..d098d076d 100644
--- a/app/src/main/java/org/linphone/core/CoreContext.kt
+++ b/app/src/main/java/org/linphone/core/CoreContext.kt
@@ -87,7 +87,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
"$sdkVersion ($sdkBranch, $sdkBuildType)"
}
- val collator = Collator.getInstance()
+ val collator: Collator = Collator.getInstance()
val contactsManager: ContactsManager by lazy {
ContactsManager(context)
}
@@ -136,29 +136,9 @@ class CoreContext(val context: Context, coreConfig: Config) {
) {
Log.i("[Context] Call state changed [$state]")
if (state == Call.State.IncomingReceived || state == Call.State.IncomingEarlyMedia) {
- if (!corePreferences.useTelecomManager) { // Can't use the following call with Telecom Manager API as it will "fake" GSM calls
- var gsmCallActive = false
- if (::phoneStateListener.isInitialized) {
- gsmCallActive = phoneStateListener.isInCall()
- }
-
- if (gsmCallActive) {
- Log.w("[Context] Refusing the call with reason busy because a GSM call is active")
- call.decline(Reason.Busy)
- return
- }
- } else {
- if (TelecomHelper.exists()) {
- if (!TelecomHelper.get().isIncomingCallPermitted() ||
- TelecomHelper.get().isInManagedCall()
- ) {
- Log.w("[Context] Refusing the call with reason busy because Telecom Manager will reject the call")
- call.decline(Reason.Busy)
- return
- }
- } else {
- Log.e("[Context] Telecom Manager singleton wasn't created!")
- }
+ if (declineCallDueToGsmActiveCall()) {
+ call.decline(Reason.Busy)
+ return
}
// Starting SDK 24 (Android 7.0) we rely on the fullscreen intent of the call incoming notification
@@ -209,7 +189,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
}
}
- if (corePreferences.routeAudioToSpeakerWhenVideoIsEnabled && call.currentParams.videoEnabled()) {
+ if (corePreferences.routeAudioToSpeakerWhenVideoIsEnabled && call.currentParams.isVideoEnabled) {
// Do not turn speaker on when video is enabled if headset or bluetooth is used
if (!AudioRouteUtils.isHeadsetAudioRouteAvailable() && !AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed(
call
@@ -306,15 +286,19 @@ class CoreContext(val context: Context, coreConfig: Config) {
fun start(isPush: Boolean = false) {
Log.i("[Context] Starting")
- notificationsManager.onCoreReady()
-
core.addListener(listener)
// CoreContext listener must be added first!
if (Version.sdkAboveOrEqual(Version.API26_O_80) && corePreferences.useTelecomManager) {
- Log.i("[Context] Creating TelecomHelper, disabling audio focus requests in AudioHelper")
- core.config.setBool("audio", "android_disable_audio_focus_requests", true)
- TelecomHelper.create(context)
+ if (Compatibility.hasTelecomManagerPermissions(context)) {
+ Log.i("[Context] Creating Telecom Helper, disabling audio focus requests in AudioHelper")
+ core.config.setBool("audio", "android_disable_audio_focus_requests", true)
+ val telecomHelper = TelecomHelper.required(context)
+ Log.i("[Context] Telecom Helper created, account is ${if (telecomHelper.isAccountEnabled()) "enabled" else "disabled"}")
+ } else {
+ Log.w("[Context] Can't create Telecom Helper, permissions have been revoked")
+ corePreferences.useTelecomManager = false
+ }
}
if (isPush) {
@@ -322,14 +306,27 @@ class CoreContext(val context: Context, coreConfig: Config) {
core.enterBackground()
}
- core.start()
-
configureCore()
+ core.start()
+
initPhoneStateListener()
+ notificationsManager.onCoreReady()
+
EmojiCompat.init(BundledEmojiCompatConfig(context))
collator.strength = Collator.NO_DECOMPOSITION
+
+ if (corePreferences.vfsEnabled) {
+ FileUtils.clearExistingPlainFiles()
+ }
+
+ if (corePreferences.keepServiceAlive) {
+ Log.i("[Context] Background mode setting is enabled, starting Service")
+ notificationsManager.startForeground()
+ }
+
+ Log.i("[Context] Started")
}
fun stop() {
@@ -344,6 +341,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
if (TelecomHelper.exists()) {
Log.i("[Context] Destroying telecom helper")
TelecomHelper.get().destroy()
+ TelecomHelper.destroy()
}
core.stop()
@@ -387,6 +385,12 @@ class CoreContext(val context: Context, coreConfig: Config) {
core.limeX3DhServerUrl = url
}
}
+
+ // Ensure we allow CPIM messages in basic chat rooms
+ val newParams = account.params.clone()
+ newParams.isCpimInBasicChatRoomEnabled = true
+ account.params = newParams
+ Log.i("[Context] CPIM allowed in basic chat rooms for account ${newParams.identityAddress?.asStringUriOnly()}")
}
}
@@ -432,15 +436,41 @@ class CoreContext(val context: Context, coreConfig: Config) {
}
}
+ fun declineCallDueToGsmActiveCall(): Boolean {
+ if (!corePreferences.useTelecomManager) { // Can't use the following call with Telecom Manager API as it will "fake" GSM calls
+ var gsmCallActive = false
+ if (::phoneStateListener.isInitialized) {
+ gsmCallActive = phoneStateListener.isInCall()
+ }
+
+ if (gsmCallActive) {
+ Log.w("[Context] Refusing the call with reason busy because a GSM call is active")
+ return true
+ }
+ } else {
+ if (TelecomHelper.exists()) {
+ if (!TelecomHelper.get().isIncomingCallPermitted() ||
+ TelecomHelper.get().isInManagedCall()
+ ) {
+ Log.w("[Context] Refusing the call with reason busy because Telecom Manager will reject the call")
+ return true
+ }
+ } else {
+ Log.e("[Context] Telecom Manager singleton wasn't created!")
+ }
+ }
+ return false
+ }
+
fun answerCallVideoUpdateRequest(call: Call, accept: Boolean) {
val params = core.createCallParams(call)
if (accept) {
- params?.enableVideo(true)
- core.enableVideoCapture(true)
- core.enableVideoDisplay(true)
+ params?.isVideoEnabled = true
+ core.isVideoCaptureEnabled = true
+ core.isVideoDisplayEnabled = true
} else {
- params?.enableVideo(false)
+ params?.isVideoEnabled = false
}
call.acceptUpdate(params)
@@ -452,7 +482,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
params?.recordFile = LinphoneUtils.getRecordingFilePathForAddress(call.remoteAddress)
if (LinphoneUtils.checkIfNetworkHasLowBandwidth(context)) {
Log.w("[Context] Enabling low bandwidth mode!")
- params?.enableLowBandwidth(true)
+ params?.isLowBandwidthEnabled = true
}
call.acceptWithParams(params)
}
@@ -529,7 +559,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
}
if (LinphoneUtils.checkIfNetworkHasLowBandwidth(context)) {
Log.w("[Context] Enabling low bandwidth mode!")
- params.enableLowBandwidth(true)
+ params.isLowBandwidthEnabled = true
}
params.recordFile = LinphoneUtils.getRecordingFilePathForAddress(address)
@@ -543,7 +573,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
}
if (corePreferences.sendEarlyMedia) {
- params.enableEarlyMediaSending(true)
+ params.isEarlyMediaSendingEnabled = true
}
val call = core.inviteAddressWithParams(address, params)
@@ -582,7 +612,7 @@ class CoreContext(val context: Context, coreConfig: Config) {
return if (conference != null && conference.isIn) {
conference.currentParams.isVideoEnabled
} else {
- core.currentCall?.currentParams?.videoEnabled() ?: false
+ core.currentCall?.currentParams?.isVideoEnabled ?: false
}
}
diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt
index f66c22aa3..e69d23f9b 100644
--- a/app/src/main/java/org/linphone/core/CorePreferences.kt
+++ b/app/src/main/java/org/linphone/core/CorePreferences.kt
@@ -317,8 +317,15 @@ class CorePreferences constructor(private val context: Context) {
config.setBool("audio", "android_disable_audio_focus_requests", value)
}
+ // We will try to auto enable Telecom Manager feature, but in case user disables it don't try again
+ var manuallyDisabledTelecomManager: Boolean
+ get() = config.getBool("app", "user_disabled_self_managed_telecom_manager", false)
+ set(value) {
+ config.setBool("app", "user_disabled_self_managed_telecom_manager", value)
+ }
+
var fullScreenCallUI: Boolean
- get() = config.getBool("app", "full_screen_call", true)
+ get() = config.getBool("app", "full_screen_call", false)
set(value) {
config.setBool("app", "full_screen_call", value)
}
@@ -424,6 +431,11 @@ class CorePreferences constructor(private val context: Context) {
val fetchContactsFromDefaultDirectory: Boolean
get() = config.getBool("app", "fetch_contacts_from_default_directory", true)
+ // From Android Contact APIs we can also retrieve the internationalized phone number
+ // By default we display the same value as the native address book app
+ val preferNormalizedPhoneNumbersFromAddressBook: Boolean
+ get() = config.getBool("app", "prefer_normalized_phone_numbers_from_address_book", false)
+
val hideStaticImageCamera: Boolean
get() = config.getBool("app", "hide_static_image_camera", true)
@@ -448,6 +460,11 @@ class CorePreferences constructor(private val context: Context) {
val useEphemeralPerDeviceMode: Boolean
get() = config.getBool("app", "ephemeral_chat_messages_settings_per_device", true)
+ // If enabled user will see all ringtones bundled in our SDK
+ // and will be able to choose which one to use if not using it's device's default
+ val showAllRingtones: Boolean
+ get() = config.getBool("app", "show_all_available_ringtones", false)
+
/* Default values related */
val echoCancellerCalibration: Int
@@ -566,8 +583,11 @@ class CorePreferences constructor(private val context: Context) {
val defaultValuesPath: String
get() = context.filesDir.absolutePath + "/assistant_default_values"
- val ringtonePath: String
- get() = context.filesDir.absolutePath + "/share/sounds/linphone/rings/notes_of_the_optimistic.mkv"
+ val ringtonesPath: String
+ get() = context.filesDir.absolutePath + "/share/sounds/linphone/rings/"
+
+ val defaultRingtonePath: String
+ get() = ringtonesPath + "notes_of_the_optimistic.mkv"
val userCertificatesPath: String
get() = context.filesDir.absolutePath + "/user-certs"
diff --git a/app/src/main/java/org/linphone/core/CoreService.kt b/app/src/main/java/org/linphone/core/CoreService.kt
index 1eeac37da..56411a98c 100644
--- a/app/src/main/java/org/linphone/core/CoreService.kt
+++ b/app/src/main/java/org/linphone/core/CoreService.kt
@@ -67,6 +67,8 @@ class CoreService : CoreService() {
} else {
Log.w("[Service] Task removed but Core in not in background, skipping")
}
+ } else {
+ Log.i("[Service] Task removed but we were asked to keep the service alive, so doing nothing")
}
super.onTaskRemoved(rootIntent)
diff --git a/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt b/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt
index f0161a4c3..5215bdf2d 100644
--- a/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt
+++ b/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt
@@ -19,6 +19,7 @@
*/
package org.linphone.notifications
+import android.app.NotificationManager
import android.app.RemoteInput
import android.content.BroadcastReceiver
import android.content.Context
@@ -33,13 +34,13 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
val notificationId = intent.getIntExtra(NotificationsManager.INTENT_NOTIF_ID, 0)
if (intent.action == NotificationsManager.INTENT_REPLY_NOTIF_ACTION || intent.action == NotificationsManager.INTENT_MARK_AS_READ_ACTION) {
- handleChatIntent(intent, notificationId)
+ handleChatIntent(context, intent, notificationId)
} else if (intent.action == NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION || intent.action == NotificationsManager.INTENT_HANGUP_CALL_NOTIF_ACTION) {
handleCallIntent(intent)
}
}
- private fun handleChatIntent(intent: Intent, notificationId: Int) {
+ private fun handleChatIntent(context: Context, intent: Intent, notificationId: Int) {
val remoteSipAddress = intent.getStringExtra(NotificationsManager.INTENT_REMOTE_ADDRESS)
if (remoteSipAddress == null) {
Log.e("[Notification Broadcast Receiver] Remote SIP address is null for notification id $notificationId")
@@ -84,7 +85,11 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
msg.send()
Log.i("[Notification Broadcast Receiver] Reply sent for notif id $notificationId")
} else {
- coreContext.notificationsManager.dismissChatNotification(room)
+ if (!coreContext.notificationsManager.dismissChatNotification(room)) {
+ Log.w("[Notification Broadcast Receiver] Notifications Manager failed to cancel notification")
+ val notificationManager = context.getSystemService(NotificationManager::class.java)
+ notificationManager.cancel(NotificationsManager.CHAT_TAG, notificationId)
+ }
}
}
@@ -107,7 +112,13 @@ class NotificationBroadcastReceiver : BroadcastReceiver() {
if (intent.action == NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION) {
coreContext.answerCall(call)
} else {
- if (call.state == Call.State.IncomingReceived || call.state == Call.State.IncomingEarlyMedia) coreContext.declineCall(call) else coreContext.terminateCall(call)
+ if (call.state == Call.State.IncomingReceived ||
+ call.state == Call.State.IncomingEarlyMedia
+ ) {
+ coreContext.declineCall(call)
+ } else {
+ coreContext.terminateCall(call)
+ }
}
}
diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt
index 28c26b141..5d462323d 100644
--- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt
+++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt
@@ -20,6 +20,7 @@
package org.linphone.notifications
import android.app.Notification
+import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
@@ -27,7 +28,6 @@ import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.webkit.MimeTypeMap
-import android.widget.RemoteViews
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.Person
@@ -55,7 +55,7 @@ import org.linphone.utils.FileUtils
import org.linphone.utils.ImageUtils
import org.linphone.utils.LinphoneUtils
-private class Notifiable(val notificationId: Int) {
+class Notifiable(val notificationId: Int) {
val messages: ArrayList = arrayListOf()
var isGroup: Boolean = false
@@ -65,7 +65,7 @@ private class Notifiable(val notificationId: Int) {
var remoteAddress: String? = null
}
-private class NotifiableMessage(
+class NotifiableMessage(
var message: String,
val contact: Contact?,
val sender: String,
@@ -90,6 +90,9 @@ class NotificationsManager(private val context: Context) {
private const val SERVICE_NOTIF_ID = 1
private const val MISSED_CALLS_NOTIF_ID = 2
+
+ const val CHAT_TAG = "Chat"
+ private const val MISSED_CALL_TAG = "Missed call"
}
private val notificationManager: NotificationManagerCompat by lazy {
@@ -97,6 +100,8 @@ class NotificationsManager(private val context: Context) {
}
private val chatNotificationsMap: HashMap = HashMap()
private val callNotificationsMap: HashMap = HashMap()
+ private val previousChatNotifications: ArrayList = arrayListOf()
+ private val chatBubbleNotifications: ArrayList = arrayListOf()
private var currentForegroundServiceNotificationId: Int = 0
private var serviceNotification: Notification? = null
@@ -105,8 +110,6 @@ class NotificationsManager(private val context: Context) {
var currentlyDisplayedChatRoomAddress: String? = null
- var dismissNotificationUponReadChatRoom: Boolean = true
-
private val listener: CoreListenerStub = object : CoreListenerStub() {
override fun onCallStateChanged(
core: Core,
@@ -121,7 +124,7 @@ class NotificationsManager(private val context: Context) {
return
}
- when (state) {
+ when (call.state) {
Call.State.IncomingEarlyMedia, Call.State.IncomingReceived -> displayIncomingCallNotification(call)
Call.State.End, Call.State.Error -> dismissCallNotification(call)
Call.State.Released -> {
@@ -168,11 +171,23 @@ class NotificationsManager(private val context: Context) {
}
override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) {
- if (dismissNotificationUponReadChatRoom) {
- Log.i("[Notifications Manager] Chat room [$chatRoom] has been marked as read, removing notification if any")
- dismissChatNotification(chatRoom)
+ val address = chatRoom.peerAddress.asStringUriOnly()
+ val notifiable = chatNotificationsMap[address]
+ if (notifiable != null) {
+ if (chatBubbleNotifications.contains(notifiable.notificationId)) {
+ Log.i("[Notifications Manager] Chat room [$chatRoom] has been marked as read, not removing notification because of a chat bubble")
+ } else {
+ Log.i("[Notifications Manager] Chat room [$chatRoom] has been marked as read, removing notification if any")
+ dismissChatNotification(chatRoom)
+ }
} else {
- Log.i("[Notifications Manager] Chat room [$chatRoom] has been marked as read, not removing notification, maybe because of a chat bubble?")
+ val notificationId = chatRoom.creationTime.toInt()
+ if (chatBubbleNotifications.contains(notificationId)) {
+ Log.i("[Notifications Manager] Chat room [$chatRoom] has been marked as read but no notifiable found, not removing notification because of a chat bubble")
+ } else {
+ Log.i("[Notifications Manager] Chat room [$chatRoom] has been marked as read but no notifiable found, removing notification if any")
+ dismissChatNotification(chatRoom)
+ }
}
}
}
@@ -198,17 +213,28 @@ class NotificationsManager(private val context: Context) {
displayReplyMessageNotification(message, notifiable)
} else {
Log.e("[Notifications Manager] Couldn't find notification for chat room $address")
- cancel(id)
+ cancel(id, CHAT_TAG)
}
} else if (state == ChatMessage.State.NotDelivered) {
Log.e("[Notifications Manager] Reply wasn't delivered")
- cancel(id)
+ cancel(id, CHAT_TAG)
}
}
}
init {
Compatibility.createNotificationChannels(context, notificationManager)
+
+ val manager = context.getSystemService(NotificationManager::class.java) as NotificationManager
+ for (notification in manager.activeNotifications) {
+ if (notification.tag.isNullOrEmpty()) { // We use null tag for call notifications otherwise it will create duplicates when used with Service.startForeground()...
+ Log.w("[Notifications Manager] Found existing call? notification [${notification.id}], cancelling it")
+ manager.cancel(notification.tag, notification.id)
+ } else if (notification.tag == CHAT_TAG) {
+ Log.i("[Notifications Manager] Found existing chat notification [${notification.id}]")
+ previousChatNotifications.add(notification.id)
+ }
+ }
}
fun onCoreReady() {
@@ -222,26 +248,29 @@ class NotificationsManager(private val context: Context) {
// causing the notification to be missed by the user...
Log.i("[Notifications Manager] Getting destroyed, clearing foreground Service & call notifications")
- if (currentForegroundServiceNotificationId > 0) {
- notificationManager.cancel(currentForegroundServiceNotificationId)
+ if (currentForegroundServiceNotificationId > 0 && !corePreferences.keepServiceAlive) {
+ Log.i("[Notifications Manager] Clearing foreground Service")
+ stopForegroundNotification()
}
- for (notifiable in callNotificationsMap.values) {
- notificationManager.cancel(notifiable.notificationId)
+ if (callNotificationsMap.size > 0) {
+ Log.i("[Notifications Manager] Clearing call notifications")
+ for (notifiable in callNotificationsMap.values) {
+ notificationManager.cancel(notifiable.notificationId)
+ }
}
- stopForegroundNotification()
coreContext.core.removeListener(listener)
}
- private fun notify(id: Int, notification: Notification) {
- Log.i("[Notifications Manager] Notifying $id")
- notificationManager.notify(id, notification)
+ private fun notify(id: Int, notification: Notification, tag: String? = null) {
+ Log.i("[Notifications Manager] Notifying [$id] with tag [$tag]")
+ notificationManager.notify(tag, id, notification)
}
- fun cancel(id: Int) {
- Log.i("[Notifications Manager] Canceling $id")
- notificationManager.cancel(id)
+ fun cancel(id: Int, tag: String? = null) {
+ Log.i("[Notifications Manager] Canceling [$id] with tag [$tag]")
+ notificationManager.cancel(tag, id)
}
fun resetChatNotificationCounterForSipUri(sipUri: String) {
@@ -259,7 +288,13 @@ class NotificationsManager(private val context: Context) {
Log.w("[Notifications Manager] Can't start service as foreground without a service, starting it now")
val intent = Intent()
intent.setClass(coreContext.context, CoreService::class.java)
- coreContext.context.startService(intent)
+ try {
+ Compatibility.startForegroundService(coreContext.context, intent)
+ } catch (ise: IllegalStateException) {
+ Log.e("[Notifications Manager] Failed to start Service: $ise")
+ } catch (se: SecurityException) {
+ Log.e("[Notifications Manager] Failed to start Service: $se")
+ }
}
}
@@ -267,34 +302,41 @@ class NotificationsManager(private val context: Context) {
service = coreService
when {
currentForegroundServiceNotificationId != 0 -> {
- Log.e("[Notifications Manager] There is already a foreground service notification")
+ if (currentForegroundServiceNotificationId != SERVICE_NOTIF_ID) {
+ Log.e("[Notifications Manager] There is already a foreground service notification [$currentForegroundServiceNotificationId]")
+ } else {
+ Log.i("[Notifications Manager] There is already a foreground service notification, no need to use the call notification to keep Service alive")
+ }
}
coreContext.core.callsNb > 0 -> {
// When this method will be called, we won't have any notification yet
val call = coreContext.core.currentCall ?: coreContext.core.calls[0]
when (call.state) {
Call.State.IncomingReceived, Call.State.IncomingEarlyMedia -> {
- displayIncomingCallNotification(call, true)
+ Log.i("[Notifications Manager] Waiting for call to be in state Connected before creating service notification")
+ }
+ else -> {
+ Log.i("[Notifications Manager] Creating call notification to be used as foreground service")
+ displayCallNotification(call, true)
}
- else -> displayCallNotification(call, true)
}
}
}
}
fun startForeground(coreService: CoreService, useAutoStartDescription: Boolean = true) {
- Log.i("[Notifications Manager] Starting service as foreground")
if (serviceNotification == null) {
createServiceNotification(useAutoStartDescription)
}
currentForegroundServiceNotificationId = SERVICE_NOTIF_ID
+ Log.i("[Notifications Manager] Starting service as foreground [$currentForegroundServiceNotificationId]")
coreService.startForeground(currentForegroundServiceNotificationId, serviceNotification)
service = coreService
}
private fun startForeground(notificationId: Int, callNotification: Notification) {
if (currentForegroundServiceNotificationId == 0 && service != null) {
- Log.i("[Notifications Manager] Starting service as foreground using call notification")
+ Log.i("[Notifications Manager] Starting service as foreground using call notification [$notificationId]")
currentForegroundServiceNotificationId = notificationId
service?.startForeground(currentForegroundServiceNotificationId, callNotification)
}
@@ -302,7 +344,7 @@ class NotificationsManager(private val context: Context) {
private fun stopForegroundNotification() {
if (service != null) {
- Log.i("[Notifications Manager] Stopping service as foreground")
+ Log.i("[Notifications Manager] Stopping service as foreground [$currentForegroundServiceNotificationId]")
service?.stopForeground(true)
currentForegroundServiceNotificationId = 0
}
@@ -310,14 +352,14 @@ class NotificationsManager(private val context: Context) {
fun stopForegroundNotificationIfPossible() {
if (service != null && currentForegroundServiceNotificationId == SERVICE_NOTIF_ID && !corePreferences.keepServiceAlive) {
- Log.i("[Notifications Manager] Stopping auto-started service notification")
+ Log.i("[Notifications Manager] Stopping auto-started service notification [$currentForegroundServiceNotificationId]")
stopForegroundNotification()
}
}
fun stopCallForeground() {
if (service != null && currentForegroundServiceNotificationId != SERVICE_NOTIF_ID && !corePreferences.keepServiceAlive) {
- Log.i("[Notifications Manager] Stopping call notification used as foreground service")
+ Log.i("[Notifications Manager] Stopping call notification [$currentForegroundServiceNotificationId] used as foreground service")
stopForegroundNotification()
}
}
@@ -371,14 +413,14 @@ class NotificationsManager(private val context: Context) {
return notifiable
}
- private fun getPerson(contact: Contact?, displayName: String, picture: Bitmap?): Person {
+ fun getPerson(contact: Contact?, displayName: String, picture: Bitmap?): Person {
return if (contact != null) {
contact.getPerson()
} else {
val builder = Person.Builder().setName(displayName)
val userIcon =
if (picture != null) {
- IconCompat.createWithBitmap(picture)
+ IconCompat.createWithAdaptiveBitmap(picture)
} else {
IconCompat.createWithResource(context, R.drawable.avatar)
}
@@ -388,18 +430,17 @@ class NotificationsManager(private val context: Context) {
}
private fun displayIncomingCallNotification(call: Call, useAsForeground: Boolean = false) {
- val address = LinphoneUtils.getDisplayableAddress(call.remoteAddress)
- val notifiable = getNotifiableForCall(call)
-
- if (notifiable.notificationId == currentForegroundServiceNotificationId) {
- Log.w("[Notifications Manager] Incoming call notification already displayed by foreground service, skipping")
+ if (coreContext.declineCallDueToGsmActiveCall()) {
+ Log.w("[Notifications Manager] Call will be declined, do not show incoming call notification")
return
}
- val contact: Contact? = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
- val pictureUri = contact?.getContactThumbnailPictureUri()
- val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri)
- val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress)
+ val notifiable = getNotifiableForCall(call)
+ if (notifiable.notificationId == currentForegroundServiceNotificationId) {
+ Log.e("[Notifications Manager] There is already a Service foreground notification for an incoming call, cancelling it")
+ cancel(notifiable.notificationId)
+ currentForegroundServiceNotificationId = 0
+ }
val incomingCallNotificationIntent = Intent(context, IncomingCallActivity::class.java)
incomingCallNotificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@@ -410,45 +451,12 @@ class NotificationsManager(private val context: Context) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
- val notificationLayoutHeadsUp = RemoteViews(context.packageName, R.layout.call_incoming_notification_heads_up)
- notificationLayoutHeadsUp.setTextViewText(R.id.caller, displayName)
- notificationLayoutHeadsUp.setTextViewText(R.id.sip_uri, address)
- notificationLayoutHeadsUp.setTextViewText(R.id.incoming_call_info, context.getString(R.string.incoming_call_notification_title))
-
- if (roundPicture != null) {
- notificationLayoutHeadsUp.setImageViewBitmap(R.id.caller_picture, roundPicture)
- }
-
- val builder = NotificationCompat.Builder(context, context.getString(R.string.notification_channel_incoming_call_id))
- .setStyle(NotificationCompat.DecoratedCustomViewStyle())
- .addPerson(getPerson(contact, displayName, roundPicture))
- .setSmallIcon(R.drawable.topbar_call_notification)
- .setContentTitle(displayName)
- .setContentText(context.getString(R.string.incoming_call_notification_title))
- .setCategory(NotificationCompat.CATEGORY_CALL)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setPriority(NotificationCompat.PRIORITY_HIGH)
- .setWhen(System.currentTimeMillis())
- .setAutoCancel(false)
- .setShowWhen(true)
- .setOngoing(true)
- .setColor(ContextCompat.getColor(context, R.color.primary_color))
- .setFullScreenIntent(pendingIntent, true)
- .addAction(getCallDeclineAction(notifiable))
- .addAction(getCallAnswerAction(notifiable))
- .setCustomHeadsUpContentView(notificationLayoutHeadsUp)
-
- if (!corePreferences.preventInterfaceFromShowingUp) {
- builder.setContentIntent(pendingIntent)
- }
-
- val notification = builder.build()
-
- Log.i("[Notifications Manager] Notifying incoming call notification")
+ val notification = Compatibility.createIncomingCallNotification(context, call, notifiable, pendingIntent, this)
+ Log.i("[Notifications Manager] Notifying incoming call notification [${notifiable.notificationId}]")
notify(notifiable.notificationId, notification)
if (useAsForeground) {
- Log.i("[Notifications Manager] Notifying incoming call notification for foreground service")
+ Log.i("[Notifications Manager] Notifying incoming call notification for foreground service [${notifiable.notificationId}]")
startForeground(notifiable.notificationId, notification)
}
}
@@ -492,17 +500,28 @@ class NotificationsManager(private val context: Context) {
}
val notification = builder.build()
-
- notify(MISSED_CALLS_NOTIF_ID, notification)
+ notify(MISSED_CALLS_NOTIF_ID, notification, MISSED_CALL_TAG)
}
fun dismissMissedCallNotification() {
- cancel(MISSED_CALLS_NOTIF_ID)
+ cancel(MISSED_CALLS_NOTIF_ID, MISSED_CALL_TAG)
}
fun displayCallNotification(call: Call, useAsForeground: Boolean = false) {
val notifiable = getNotifiableForCall(call)
+ val callActivity: Class<*> = when (call.state) {
+ Call.State.Paused, Call.State.Pausing, Call.State.PausedByRemote -> {
+ CallActivity::class.java
+ }
+ Call.State.OutgoingRinging, Call.State.OutgoingProgress, Call.State.OutgoingInit, Call.State.OutgoingEarlyMedia -> {
+ OutgoingCallActivity::class.java
+ }
+ else -> {
+ CallActivity::class.java
+ }
+ }
+
val serviceChannel = context.getString(R.string.notification_channel_service_id)
val channelToUse = when (val serviceChannelImportance = Compatibility.getChannelImportance(notificationManager, serviceChannel)) {
NotificationManagerCompat.IMPORTANCE_NONE -> {
@@ -520,40 +539,6 @@ class NotificationsManager(private val context: Context) {
}
}
- val contact: Contact? = coreContext.contactsManager.findContactByAddress(call.remoteAddress)
- val pictureUri = contact?.getContactThumbnailPictureUri()
- val roundPicture = ImageUtils.getRoundBitmapFromUri(context, pictureUri)
- val displayName = contact?.fullName ?: LinphoneUtils.getDisplayName(call.remoteAddress)
-
- val stringResourceId: Int
- val iconResourceId: Int
- val callActivity: Class<*>
- when (call.state) {
- Call.State.Paused, Call.State.Pausing, Call.State.PausedByRemote -> {
- callActivity = CallActivity::class.java
- stringResourceId = R.string.call_notification_paused
- iconResourceId = R.drawable.topbar_call_paused_notification
- }
- Call.State.OutgoingRinging, Call.State.OutgoingProgress, Call.State.OutgoingInit, Call.State.OutgoingEarlyMedia -> {
- callActivity = OutgoingCallActivity::class.java
- stringResourceId = R.string.call_notification_outgoing
- iconResourceId = if (call.params.videoEnabled()) {
- R.drawable.topbar_videocall_notification
- } else {
- R.drawable.topbar_call_notification
- }
- }
- else -> {
- callActivity = CallActivity::class.java
- stringResourceId = R.string.call_notification_active
- iconResourceId = if (call.currentParams.videoEnabled()) {
- R.drawable.topbar_videocall_notification
- } else {
- R.drawable.topbar_call_notification
- }
- }
- }
-
val callNotificationIntent = Intent(context, callActivity)
callNotificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
val pendingIntent = PendingIntent.getActivity(
@@ -563,33 +548,12 @@ class NotificationsManager(private val context: Context) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
- val builder = NotificationCompat.Builder(
- context, channelToUse
- )
- .setContentTitle(contact?.fullName ?: displayName)
- .setContentText(context.getString(stringResourceId))
- .setSmallIcon(iconResourceId)
- .setLargeIcon(roundPicture)
- .addPerson(getPerson(contact, displayName, roundPicture))
- .setAutoCancel(false)
- .setCategory(NotificationCompat.CATEGORY_CALL)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setPriority(NotificationCompat.PRIORITY_LOW)
- .setWhen(System.currentTimeMillis())
- .setShowWhen(true)
- .setOngoing(true)
- .setColor(ContextCompat.getColor(context, R.color.notification_led_color))
- .addAction(getCallDeclineAction(notifiable))
-
- if (!corePreferences.preventInterfaceFromShowingUp) {
- builder.setContentIntent(pendingIntent)
- }
-
- val notification = builder.build()
-
+ val notification = Compatibility.createCallNotification(context, call, notifiable, pendingIntent, channelToUse, this)
+ Log.i("[Notifications Manager] Notifying call notification [${notifiable.notificationId}]")
notify(notifiable.notificationId, notification)
- if (useAsForeground) {
+ if (useAsForeground || (service != null && currentForegroundServiceNotificationId == 0)) {
+ Log.i("[Notifications Manager] Notifying call notification for foreground service [${notifiable.notificationId}]")
startForeground(notifiable.notificationId, notification)
}
}
@@ -633,12 +597,12 @@ class NotificationsManager(private val context: Context) {
context,
notifiable.notificationId,
target,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+ Compatibility.getUpdateCurrentPendingIntentFlag()
)
val id = LinphoneUtils.getChatRoomId(room.localAddress, room.peerAddress)
val notification = createMessageNotification(notifiable, pendingIntent, bubbleIntent, id)
- notify(notifiable.notificationId, notification)
+ notify(notifiable.notificationId, notification, CHAT_TAG)
}
private fun displayIncomingChatNotification(room: ChatRoom, message: ChatMessage) {
@@ -739,13 +703,37 @@ class NotificationsManager(private val context: Context) {
displayChatNotifiable(message.chatRoom, notifiable)
}
- fun dismissChatNotification(room: ChatRoom) {
+ fun dismissChatNotification(room: ChatRoom): Boolean {
val address = room.peerAddress.asStringUriOnly()
val notifiable: Notifiable? = chatNotificationsMap[address]
if (notifiable != null) {
Log.i("[Notifications Manager] Dismissing notification for chat room $room with id ${notifiable.notificationId}")
notifiable.messages.clear()
- cancel(notifiable.notificationId)
+ cancel(notifiable.notificationId, CHAT_TAG)
+ return true
+ } else {
+ val previousNotificationId = previousChatNotifications.find { id -> id == room.creationTime.toInt() }
+ if (previousNotificationId != null) {
+ if (chatBubbleNotifications.contains(previousNotificationId)) {
+ Log.i("[Notifications Manager] Found previous notification with same ID [$previousNotificationId] but not cancelling it as it's ID is in chat bubbles list")
+ } else {
+ Log.i("[Notifications Manager] Found previous notification with same ID [$previousNotificationId], canceling it")
+ cancel(previousNotificationId, CHAT_TAG)
+ }
+ return true
+ }
+ }
+ return false
+ }
+
+ fun changeDismissNotificationUponReadForChatRoom(chatRoom: ChatRoom, dismiss: Boolean) {
+ val notificationId = chatRoom.creationTime.toInt()
+ if (dismiss) {
+ Log.i("[Notifications Manager] Allow notification with id [$notificationId] to be dismissed when chat room will be marked as read, used for chat bubble")
+ chatBubbleNotifications.add(notificationId)
+ } else {
+ Log.i("[Notifications Manager] Prevent notification with id [$notificationId] from being dismissed when chat room will be marked as read, used for chat bubble")
+ chatBubbleNotifications.remove(notificationId)
}
}
@@ -831,43 +819,47 @@ class NotificationsManager(private val context: Context) {
/* Notifications actions */
- private fun getCallAnswerAction(notifiable: Notifiable): NotificationCompat.Action {
+ fun getCallAnswerPendingIntent(notifiable: Notifiable): PendingIntent {
val answerIntent = Intent(context, NotificationBroadcastReceiver::class.java)
answerIntent.action = INTENT_ANSWER_CALL_NOTIF_ACTION
answerIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId)
answerIntent.putExtra(INTENT_REMOTE_ADDRESS, notifiable.remoteAddress)
- val answerPendingIntent = PendingIntent.getBroadcast(
+ return PendingIntent.getBroadcast(
context,
notifiable.notificationId,
answerIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
+ }
+ fun getCallAnswerAction(notifiable: Notifiable): NotificationCompat.Action {
return NotificationCompat.Action.Builder(
R.drawable.call_audio_start,
context.getString(R.string.incoming_call_notification_answer_action_label),
- answerPendingIntent
+ getCallAnswerPendingIntent(notifiable)
).build()
}
- private fun getCallDeclineAction(notifiable: Notifiable): NotificationCompat.Action {
+ fun getCallDeclinePendingIntent(notifiable: Notifiable): PendingIntent {
val hangupIntent = Intent(context, NotificationBroadcastReceiver::class.java)
hangupIntent.action = INTENT_HANGUP_CALL_NOTIF_ACTION
hangupIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId)
hangupIntent.putExtra(INTENT_REMOTE_ADDRESS, notifiable.remoteAddress)
- val hangupPendingIntent = PendingIntent.getBroadcast(
+ return PendingIntent.getBroadcast(
context,
notifiable.notificationId,
hangupIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
+ }
+ fun getCallDeclineAction(notifiable: Notifiable): NotificationCompat.Action {
return NotificationCompat.Action.Builder(
R.drawable.call_hangup,
context.getString(R.string.incoming_call_notification_hangup_action_label),
- hangupPendingIntent
+ getCallDeclinePendingIntent(notifiable)
).build()
}
@@ -888,7 +880,7 @@ class NotificationsManager(private val context: Context) {
context,
notifiable.notificationId,
replyIntent,
- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
+ Compatibility.getUpdateCurrentPendingIntentFlag()
)
return NotificationCompat.Action.Builder(
R.drawable.chat_send_over,
diff --git a/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt b/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt
index 05e7fe67f..2173faff3 100644
--- a/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt
+++ b/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt
@@ -102,6 +102,11 @@ class NativeCallWrapper(var callId: String) : Connection() {
getCall()?.terminate() ?: selfDestroy()
}
+ override fun onSilence() {
+ Log.i("[Connection] Call with id: $callId asked to be silenced")
+ coreContext.core.stopRinging()
+ }
+
private fun getCall(): Call? {
return coreContext.core.getCallByCallid(callId)
}
diff --git a/app/src/main/java/org/linphone/telecom/TelecomConnectionService.kt b/app/src/main/java/org/linphone/telecom/TelecomConnectionService.kt
index ac8f8da9c..fa3f8c4de 100644
--- a/app/src/main/java/org/linphone/telecom/TelecomConnectionService.kt
+++ b/app/src/main/java/org/linphone/telecom/TelecomConnectionService.kt
@@ -42,6 +42,7 @@ class TelecomConnectionService : ConnectionService() {
Call.State.OutgoingProgress -> {
for (connection in TelecomHelper.get().connections) {
if (connection.callId.isEmpty()) {
+ Log.i("[Telecom Connection Service] Updating connection with call ID: ${call.callLog.callId}")
connection.callId = core.currentCall?.callLog?.callId ?: ""
}
}
@@ -51,6 +52,18 @@ class TelecomConnectionService : ConnectionService() {
Call.State.Connected -> onCallConnected(call)
}
}
+
+ override fun onLastCallEnded(core: Core) {
+ val connectionsCount = TelecomHelper.get().connections.size
+ if (connectionsCount > 0) {
+ Log.w("[Telecom Connection Service] Last call ended, there is $connectionsCount connections still alive")
+ for (connection in TelecomHelper.get().connections) {
+ Log.w("[Telecom Connection Service] Destroying zombie connection ${connection.callId}")
+ connection.setDisconnected(DisconnectCause(DisconnectCause.OTHER))
+ connection.destroy()
+ }
+ }
+ }
}
override fun onCreate() {
@@ -162,8 +175,12 @@ class TelecomConnectionService : ConnectionService() {
}
private fun onCallError(call: Call) {
- val connection = TelecomHelper.get().findConnectionForCallId(call.callLog.callId)
- connection ?: return
+ val callId = call.callLog.callId
+ val connection = TelecomHelper.get().findConnectionForCallId(callId)
+ if (connection == null) {
+ Log.e("[Telecom Connection Service] Failed to find connection for call id: $callId")
+ return
+ }
TelecomHelper.get().connections.remove(connection)
connection.setDisconnected(DisconnectCause(DisconnectCause.ERROR))
@@ -171,19 +188,27 @@ class TelecomConnectionService : ConnectionService() {
}
private fun onCallEnded(call: Call) {
- val connection = TelecomHelper.get().findConnectionForCallId(call.callLog.callId)
- connection ?: return
+ val callId = call.callLog.callId
+ val connection = TelecomHelper.get().findConnectionForCallId(callId)
+ if (connection == null) {
+ Log.e("[Telecom Connection Service] Failed to find connection for call id: $callId")
+ return
+ }
TelecomHelper.get().connections.remove(connection)
val reason = call.reason
- Log.i("[Telecom Connection Service] Call ended with reason: $reason")
+ Log.i("[Telecom Connection Service] Call [$callId] ended with reason: $reason, destroying connection")
connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
connection.destroy()
}
private fun onCallConnected(call: Call) {
- val connection = TelecomHelper.get().findConnectionForCallId(call.callLog.callId)
- connection ?: return
+ val callId = call.callLog.callId
+ val connection = TelecomHelper.get().findConnectionForCallId(callId)
+ if (connection == null) {
+ Log.e("[Telecom Connection Service] Failed to find connection for call id: $callId")
+ return
+ }
if (connection.state != Connection.STATE_HOLDING) {
connection.setActive()
diff --git a/app/src/main/java/org/linphone/telecom/TelecomHelper.kt b/app/src/main/java/org/linphone/telecom/TelecomHelper.kt
index 531cb028b..c2a29d759 100644
--- a/app/src/main/java/org/linphone/telecom/TelecomHelper.kt
+++ b/app/src/main/java/org/linphone/telecom/TelecomHelper.kt
@@ -30,6 +30,7 @@ import android.telecom.PhoneAccount
import android.telecom.PhoneAccountHandle
import android.telecom.TelecomManager
import android.telecom.TelecomManager.*
+import java.lang.Exception
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.contact.Contact
@@ -159,9 +160,16 @@ class TelecomHelper private constructor(context: Context) {
ComponentName(context, TelecomConnectionService::class.java),
context.packageName
)
- val identity = coreContext.core.defaultAccount?.params?.identityAddress?.asStringUriOnly() ?: ""
+ // Take care that identity may be parsed, otherwise Android OS may crash during startup
+ // and user will have to do a factory reset...
+ val identity = coreContext.core.defaultAccount?.params?.identityAddress?.asStringUriOnly()
+ ?: coreContext.core.createPrimaryContactParsed()?.asStringUriOnly()
+ ?: "sip:linphone.android@sip.linphone.org"
+
+ val address = Uri.parse(identity)
+ ?: throw Exception("[Telecom Helper] Identity address for phone account is null!")
val account = PhoneAccount.builder(accountHandle, context.getString(R.string.app_name))
- .setAddress(Uri.parse(identity))
+ .setAddress(address)
.setIcon(Icon.createWithResource(context, R.drawable.linphone_logo_tinted))
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
.setHighlightColor(context.getColor(R.color.primary_color))
@@ -208,7 +216,7 @@ class TelecomHelper private constructor(context: Context) {
if (call.dir == Call.Dir.Outgoing) {
extras.putString(
EXTRA_CALL_BACK_NUMBER,
- call.callLog.fromAddress.asStringUriOnly()
+ call.remoteAddress.asStringUriOnly()
)
} else {
extras.putParcelable(EXTRA_INCOMING_CALL_ADDRESS, Uri.parse(address.asStringUriOnly()))
diff --git a/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt b/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt
index d3f06fda9..d23b9759b 100644
--- a/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt
+++ b/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt
@@ -112,7 +112,11 @@ class AudioRouteUtils {
Log.i("[Audio Route Helper] Telecom Helper & matching connection found, dispatching audio route change through it")
// We will be called here again by NativeCallWrapper.onCallAudioStateChanged()
// but this time with skipTelecom = true
- Compatibility.changeAudioRouteForTelecomManager(connection, route)
+ if (!Compatibility.changeAudioRouteForTelecomManager(connection, route)) {
+ Log.w("[Audio Route Helper] Connection is already using this route internally, make the change!")
+ applyAudioRouteChange(callToUse, types)
+ changeCaptureDeviceToMatchAudioRoute(callToUse, types)
+ }
} else {
Log.w("[Audio Route Helper] Telecom Helper found but no matching connection!")
applyAudioRouteChange(callToUse, types)
diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt
index 3be9be1be..62e2cfaf4 100644
--- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt
+++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt
@@ -19,6 +19,7 @@
*/
package org.linphone.utils
+import android.app.Activity
import android.content.Context
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
@@ -29,6 +30,8 @@ import android.util.Patterns
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.inputmethod.EditorInfo
+import android.view.inputmethod.InputMethodManager
import android.widget.*
import android.widget.SeekBar.OnSeekBarChangeListener
import androidx.constraintlayout.widget.ConstraintLayout
@@ -186,6 +189,23 @@ fun editTextSetting(view: EditText, lambda: () -> Unit) {
})
}
+@BindingAdapter("onSettingImeDone")
+fun editTextImeDone(view: EditText, lambda: () -> Unit) {
+ view.setOnEditorActionListener { _, actionId, _ ->
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ lambda()
+
+ view.clearFocus()
+
+ val imm = view.context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
+ imm.hideSoftInputFromWindow(view.windowToken, 0)
+
+ return@setOnEditorActionListener true
+ }
+ false
+ }
+}
+
@BindingAdapter("onFocusChangeVisibilityOf")
fun setEditTextOnFocusChangeVisibilityOf(editText: EditText, view: View) {
editText.setOnFocusChangeListener { _, hasFocus ->
diff --git a/app/src/main/java/org/linphone/utils/FileUtils.kt b/app/src/main/java/org/linphone/utils/FileUtils.kt
index e9536c5b7..30adf0ba1 100644
--- a/app/src/main/java/org/linphone/utils/FileUtils.kt
+++ b/app/src/main/java/org/linphone/utils/FileUtils.kt
@@ -70,35 +70,56 @@ class FileUtils {
}
fun isPlainTextFile(path: String): Boolean {
- val extension = getExtensionFromFileName(path).toLowerCase(Locale.getDefault())
+ val extension = getExtensionFromFileName(path).lowercase(Locale.getDefault())
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
return type?.startsWith("text/plain") ?: false
}
fun isExtensionPdf(path: String): Boolean {
- val extension = getExtensionFromFileName(path).toLowerCase(Locale.getDefault())
+ val extension = getExtensionFromFileName(path).lowercase(Locale.getDefault())
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
return type?.startsWith("application/pdf") ?: false
}
fun isExtensionImage(path: String): Boolean {
- val extension = getExtensionFromFileName(path).toLowerCase(Locale.getDefault())
+ val extension = getExtensionFromFileName(path).lowercase(Locale.getDefault())
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
return type?.startsWith("image/") ?: false
}
fun isExtensionVideo(path: String): Boolean {
- val extension = getExtensionFromFileName(path).toLowerCase(Locale.getDefault())
+ val extension = getExtensionFromFileName(path).lowercase(Locale.getDefault())
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
return type?.startsWith("video/") ?: false
}
fun isExtensionAudio(path: String): Boolean {
- val extension = getExtensionFromFileName(path).toLowerCase(Locale.getDefault())
+ val extension = getExtensionFromFileName(path).lowercase(Locale.getDefault())
val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
return type?.startsWith("audio/") ?: false
}
+ fun clearExistingPlainFiles() {
+ for (file in coreContext.context.filesDir.listFiles().orEmpty()) {
+ if (file.path.endsWith(VFS_PLAIN_FILE_EXTENSION)) {
+ Log.w("[File Utils] Found forgotten plain file: ${file.path}, deleting it")
+ deleteFile(file.path)
+ }
+ }
+ for (file in coreContext.context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)?.listFiles().orEmpty()) {
+ if (file.path.endsWith(VFS_PLAIN_FILE_EXTENSION)) {
+ Log.w("[File Utils] Found forgotten plain file: ${file.path}, deleting it")
+ deleteFile(file.path)
+ }
+ }
+ for (file in coreContext.context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.listFiles().orEmpty()) {
+ if (file.path.endsWith(VFS_PLAIN_FILE_EXTENSION)) {
+ Log.w("[File Utils] Found forgotten plain file: ${file.path}, deleting it")
+ deleteFile(file.path)
+ }
+ }
+ }
+
fun getFileStorageDir(isPicture: Boolean = false): File {
var path: File? = null
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
@@ -470,5 +491,27 @@ class FileUtils {
}
return false
}
+
+ fun openMediaStoreFile(
+ activity: Activity,
+ contentFilePath: String,
+ newTask: Boolean = false
+ ): Boolean {
+ val intent = Intent(Intent.ACTION_VIEW)
+ val contentUri: Uri = Uri.parse(contentFilePath)
+ intent.data = contentUri
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ if (newTask) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+
+ try {
+ activity.startActivity(intent)
+ return true
+ } catch (anfe: ActivityNotFoundException) {
+ Log.e("[File Viewer] Can't open media store export in third party app: $anfe")
+ }
+ return false
+ }
}
}
diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt
index e939a085d..162811c99 100644
--- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt
+++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt
@@ -66,7 +66,7 @@ class LinphoneUtils {
fun isLimeAvailable(): Boolean {
val core = coreContext.core
- return core.limeX3DhAvailable() && core.limeX3DhEnabled() &&
+ return core.limeX3DhAvailable() && core.isLimeX3DhEnabled &&
core.limeX3DhServerUrl != null &&
core.defaultAccount?.params?.conferenceFactoryUri != null
}
@@ -81,11 +81,11 @@ class LinphoneUtils {
val defaultAccount = core.defaultAccount
val params = core.createDefaultChatRoomParams()
- params.enableGroup(false)
+ params.isGroupEnabled = false
params.backend = ChatRoomBackend.Basic
if (isSecured) {
params.subject = AppUtils.getString(R.string.chat_room_dummy_subject)
- params.enableEncryption(true)
+ params.isEncryptionEnabled = true
params.backend = ChatRoomBackend.FlexisipChat
}
diff --git a/app/src/main/java/org/linphone/utils/PatternClickableSpan.kt b/app/src/main/java/org/linphone/utils/PatternClickableSpan.kt
new file mode 100644
index 000000000..3455d18c5
--- /dev/null
+++ b/app/src/main/java/org/linphone/utils/PatternClickableSpan.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2010-2022 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.utils
+
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.text.style.ClickableSpan
+import android.view.View
+import android.widget.TextView
+import java.util.regex.Pattern
+
+class PatternClickableSpan {
+ var patterns: ArrayList = ArrayList()
+
+ inner class SpannablePatternItem(
+ var pattern: Pattern,
+ var listener: SpannableClickedListener
+ )
+
+ interface SpannableClickedListener {
+ fun onSpanClicked(text: String)
+ }
+
+ inner class StyledClickableSpan(var item: SpannablePatternItem) : ClickableSpan() {
+ override fun onClick(widget: View) {
+ val tv = widget as TextView
+ val span = tv.text as Spanned
+ val start = span.getSpanStart(this)
+ val end = span.getSpanEnd(this)
+ val text = span.subSequence(start, end)
+ item.listener.onSpanClicked(text.toString())
+ }
+ }
+
+ fun add(
+ pattern: Pattern,
+ listener: SpannableClickedListener
+ ): PatternClickableSpan {
+ patterns.add(SpannablePatternItem(pattern, listener))
+ return this
+ }
+
+ fun build(editable: CharSequence?): SpannableStringBuilder {
+ val ssb = SpannableStringBuilder(editable)
+ for (item in patterns) {
+ val matcher = item.pattern.matcher(ssb)
+ while (matcher.find()) {
+ val start = matcher.start()
+ val end = matcher.end()
+ val url = StyledClickableSpan(item)
+ ssb.setSpan(url, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ }
+ }
+ return ssb
+ }
+}
diff --git a/app/src/main/java/org/linphone/utils/PermissionHelper.kt b/app/src/main/java/org/linphone/utils/PermissionHelper.kt
index 40fad7073..3d28a3b42 100644
--- a/app/src/main/java/org/linphone/utils/PermissionHelper.kt
+++ b/app/src/main/java/org/linphone/utils/PermissionHelper.kt
@@ -21,8 +21,6 @@ package org.linphone.utils
import android.Manifest
import android.content.Context
-import android.os.Build
-import androidx.annotation.RequiresApi
import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log
@@ -75,10 +73,4 @@ class PermissionHelper private constructor(private val context: Context) {
fun hasRecordAudioPermission(): Boolean {
return hasPermission(Manifest.permission.RECORD_AUDIO)
}
-
- @RequiresApi(Build.VERSION_CODES.O)
- fun hasTelecomManagerPermissions(): Boolean {
- return hasPermission(Manifest.permission.READ_PHONE_NUMBERS) &&
- hasPermission(Manifest.permission.MANAGE_OWN_CALLS)
- }
}
diff --git a/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt b/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt
index a4f682376..031dead06 100644
--- a/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt
+++ b/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt
@@ -36,7 +36,7 @@ class RecyclerViewHeaderDecoration(private val context: Context, private val ada
parent: RecyclerView,
state: RecyclerView.State
) {
- val position = (view.layoutParams as RecyclerView.LayoutParams).viewAdapterPosition
+ val position = (view.layoutParams as RecyclerView.LayoutParams).bindingAdapterPosition
if (position != RecyclerView.NO_POSITION && adapter.displayHeaderForPosition(position)) {
val headerView: View = adapter.getHeaderViewForPosition(view.context, position)
diff --git a/app/src/main/java/org/linphone/utils/RecyclerViewSwipeUtils.kt b/app/src/main/java/org/linphone/utils/RecyclerViewSwipeUtils.kt
index 41b3f77e4..52808677a 100644
--- a/app/src/main/java/org/linphone/utils/RecyclerViewSwipeUtils.kt
+++ b/app/src/main/java/org/linphone/utils/RecyclerViewSwipeUtils.kt
@@ -46,7 +46,8 @@ class RecyclerViewSwipeConfiguration {
val textColor: Int = Color.WHITE,
val backgroundColor: Int = 0,
val icon: Int = 0,
- val iconTint: Int = 0
+ val iconTint: Int = 0,
+ val preventFor: Class<*>? = null
)
val iconMargin = 16f
@@ -61,7 +62,7 @@ class RecyclerViewSwipeConfiguration {
}
private class RecyclerViewSwipeUtilsCallback(
- direction: Int,
+ val direction: Int,
val configuration: RecyclerViewSwipeConfiguration,
val listener: RecyclerViewSwipeListener
) : ItemTouchHelper.SimpleCallback(0, direction) {
@@ -83,26 +84,38 @@ private class RecyclerViewSwipeUtilsCallback(
background.draw(canvas)
}
- val iconHorizontalMargin: Int = TypedValue.applyDimension(
+ val horizontalMargin: Int = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
configuration.iconMargin,
recyclerView.context.resources.displayMetrics
).toInt()
- var iconSize = 0
+ var iconWidth = 0
- if (configuration.leftToRightAction.icon != 0 && dX > iconHorizontalMargin) {
+ if (configuration.leftToRightAction.icon != 0) {
val icon =
- ContextCompat.getDrawable(recyclerView.context, configuration.leftToRightAction.icon)
- if (icon != null) {
- iconSize = icon.intrinsicHeight
- val halfIcon = iconSize / 2
+ ContextCompat.getDrawable(
+ recyclerView.context,
+ configuration.leftToRightAction.icon
+ )
+ iconWidth = icon?.intrinsicWidth ?: 0
+ if (icon != null && dX > iconWidth) {
+ val halfIcon = icon.intrinsicHeight / 2
val top =
viewHolder.itemView.top + ((viewHolder.itemView.bottom - viewHolder.itemView.top) / 2 - halfIcon)
+ // Icon won't move past the swipe threshold, thus indicating to the user
+ // it has reached the required distance for swipe action to be done
+ val threshold = getSwipeThreshold(viewHolder) * viewHolder.itemView.right
+ val left = if (dX < threshold) {
+ viewHolder.itemView.left + dX.toInt() - iconWidth
+ } else {
+ viewHolder.itemView.left + threshold.toInt() - iconWidth
+ }
+
icon.setBounds(
- viewHolder.itemView.left + iconHorizontalMargin,
+ left,
top,
- viewHolder.itemView.left + iconHorizontalMargin + icon.intrinsicWidth,
+ left + iconWidth,
top + icon.intrinsicHeight
)
@@ -115,7 +128,7 @@ private class RecyclerViewSwipeUtilsCallback(
}
}
- if (configuration.leftToRightAction.text.isNotEmpty() && dX > iconHorizontalMargin + iconSize) {
+ if (configuration.leftToRightAction.text.isNotEmpty() && dX > horizontalMargin + iconWidth) {
val textPaint = TextPaint()
textPaint.isAntiAlias = true
textPaint.textSize = TypedValue.applyDimension(
@@ -126,9 +139,9 @@ private class RecyclerViewSwipeUtilsCallback(
textPaint.color = configuration.leftToRightAction.textColor
textPaint.typeface = configuration.actionTextFont
- val margin = if (iconSize > 0) iconHorizontalMargin / 2 else 0
+ val margin = if (iconWidth > 0) horizontalMargin / 2 else 0
val textX =
- (viewHolder.itemView.left + iconHorizontalMargin + iconSize + margin).toFloat()
+ (viewHolder.itemView.left + horizontalMargin + iconWidth + margin).toFloat()
val textY =
(viewHolder.itemView.top + (viewHolder.itemView.bottom - viewHolder.itemView.top) / 2.0 + textPaint.textSize / 2).toFloat()
canvas.drawText(
@@ -157,30 +170,44 @@ private class RecyclerViewSwipeUtilsCallback(
background.draw(canvas)
}
- val iconHorizontalMargin: Int = TypedValue.applyDimension(
+ val horizontalMargin: Int = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
configuration.iconMargin,
recyclerView.context.resources.displayMetrics
).toInt()
- var iconSize = 0
+ var iconWidth = 0
var imageLeftBorder = viewHolder.itemView.right
- if (configuration.rightToLeftAction.icon != 0 && dX < -iconHorizontalMargin) {
+ if (configuration.rightToLeftAction.icon != 0) {
val icon =
- ContextCompat.getDrawable(recyclerView.context, configuration.rightToLeftAction.icon)
- if (icon != null) {
- iconSize = icon.intrinsicHeight
- val halfIcon = iconSize / 2
+ ContextCompat.getDrawable(
+ recyclerView.context,
+ configuration.rightToLeftAction.icon
+ )
+ iconWidth = icon?.intrinsicWidth ?: 0
+ if (icon != null && dX < viewHolder.itemView.right - iconWidth) {
+ val halfIcon = icon.intrinsicHeight / 2
val top =
viewHolder.itemView.top + ((viewHolder.itemView.bottom - viewHolder.itemView.top) / 2 - halfIcon)
- imageLeftBorder =
- viewHolder.itemView.right - iconHorizontalMargin - halfIcon * 2
+
+ // Icon won't move past the swipe threshold, thus indicating to the user
+ // it has reached the required distance for swipe action to be done
+ val threshold = -(getSwipeThreshold(viewHolder) * viewHolder.itemView.right)
+ val right = if (dX > threshold) {
+ viewHolder.itemView.right + dX.toInt()
+ } else {
+ viewHolder.itemView.right + threshold.toInt()
+ }
+ imageLeftBorder = right - icon.intrinsicWidth
+
icon.setBounds(
imageLeftBorder,
top,
- viewHolder.itemView.right - iconHorizontalMargin,
+ right,
top + icon.intrinsicHeight
)
+
+ @Suppress("DEPRECATION")
if (configuration.rightToLeftAction.iconTint != 0) icon.setColorFilter(
configuration.rightToLeftAction.iconTint,
PorterDuff.Mode.SRC_IN
@@ -188,7 +215,8 @@ private class RecyclerViewSwipeUtilsCallback(
icon.draw(canvas)
}
}
- if (configuration.rightToLeftAction.text.isNotEmpty() && dX < -iconHorizontalMargin - iconSize) {
+
+ if (configuration.rightToLeftAction.text.isNotEmpty() && dX < -horizontalMargin - iconWidth) {
val textPaint = TextPaint()
textPaint.isAntiAlias = true
textPaint.textSize = TypedValue.applyDimension(
@@ -200,7 +228,7 @@ private class RecyclerViewSwipeUtilsCallback(
textPaint.typeface = configuration.actionTextFont
val margin =
- if (imageLeftBorder == viewHolder.itemView.right) iconHorizontalMargin else iconHorizontalMargin / 2
+ if (imageLeftBorder == viewHolder.itemView.right) horizontalMargin else horizontalMargin / 2
val textX =
imageLeftBorder - textPaint.measureText(configuration.rightToLeftAction.text) - margin
val textY =
@@ -234,6 +262,32 @@ private class RecyclerViewSwipeUtilsCallback(
}
}
+ override fun getSwipeDirs(
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder
+ ): Int {
+ // Prevent swipe actions for a specific ViewHolder class if needed
+ // Used to allow swipe actions on chat messages but not events
+ var dirFlags = direction
+ if (direction and ItemTouchHelper.RIGHT != 0) {
+ val classToPrevent = configuration.leftToRightAction.preventFor
+ if (classToPrevent != null) {
+ if (classToPrevent.isInstance(viewHolder)) {
+ dirFlags = dirFlags and ItemTouchHelper.RIGHT.inv()
+ }
+ }
+ }
+ if (direction or ItemTouchHelper.LEFT != 0) {
+ val classToPrevent = configuration.rightToLeftAction.preventFor
+ if (classToPrevent != null) {
+ if (classToPrevent.isInstance(viewHolder)) {
+ dirFlags = dirFlags and ItemTouchHelper.LEFT.inv()
+ }
+ }
+ }
+ return dirFlags
+ }
+
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
@@ -253,6 +307,10 @@ private class RecyclerViewSwipeUtilsCallback(
}
}
+ override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
+ return .33f // A third of the screen is required to validate swipe move (default is .5f)
+ }
+
override fun onChildDraw(
canvas: Canvas,
recyclerView: RecyclerView,
diff --git a/app/src/main/java/org/linphone/views/SettingTextInputEditText.kt b/app/src/main/java/org/linphone/views/SettingTextInputEditText.kt
new file mode 100644
index 000000000..6340f2896
--- /dev/null
+++ b/app/src/main/java/org/linphone/views/SettingTextInputEditText.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2010-2021 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.views
+
+import android.app.Activity
+import android.content.Context
+import android.util.AttributeSet
+import android.view.inputmethod.InputMethodManager
+import com.google.android.material.textfield.TextInputEditText
+import org.linphone.activities.main.settings.SettingListener
+
+class SettingTextInputEditText : TextInputEditText {
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+
+ constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
+ context,
+ attrs,
+ defStyleAttr
+ )
+
+ fun fakeImeDone(listener: SettingListener) {
+ listener.onTextValueChanged(text.toString())
+
+ // Send IME action DONE to trigger onSettingImeDone binding adapter, but that doesn't work...
+ // val inputConnection = BaseInputConnection(this, true)
+ // inputConnection.performEditorAction(EditorInfo.IME_ACTION_DONE)
+
+ // Will make check icon to disappear thanks to onFocusChangeVisibilityOf binding adapter
+ clearFocus()
+
+ // Hide keyboard
+ val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
+ imm.hideSoftInputFromWindow(windowToken, 0)
+ }
+}
diff --git a/app/src/main/java/org/linphone/views/VoiceRecordProgressBar.kt b/app/src/main/java/org/linphone/views/VoiceRecordProgressBar.kt
index 3d918af4c..900e45e8f 100644
--- a/app/src/main/java/org/linphone/views/VoiceRecordProgressBar.kt
+++ b/app/src/main/java/org/linphone/views/VoiceRecordProgressBar.kt
@@ -205,7 +205,17 @@ class VoiceRecordProgressBar : View {
}
}
- fun setProgressDrawable(drawable: Drawable) {
+ fun setSecondaryProgressTint(color: Int) {
+ val drawable = progressDrawable
+ if (drawable != null) {
+ if (drawable is LayerDrawable) {
+ val secondaryProgressDrawable = drawable.findDrawableByLayerId(android.R.id.secondaryProgress)
+ secondaryProgressDrawable?.setTint(color)
+ }
+ }
+ }
+
+ private fun setProgressDrawable(drawable: Drawable) {
val needUpdate: Boolean = if (progressDrawable != null && drawable !== progressDrawable) {
progressDrawable?.callback = null
true
@@ -233,16 +243,6 @@ class VoiceRecordProgressBar : View {
}
}
- fun setSecondaryProgressTint(color: Int) {
- val drawable = progressDrawable
- if (drawable != null) {
- if (drawable is LayerDrawable) {
- val secondaryProgressDrawable = drawable.findDrawableByLayerId(android.R.id.secondaryProgress)
- secondaryProgressDrawable?.setTint(color)
- }
- }
- }
-
private fun refreshProgress(id: Int, progress: Int) {
var scale: Float = if (max > 0) (progress.toFloat() / max) else 0f
diff --git a/app/src/main/res/drawable-xhdpi/menu_forward_default.png b/app/src/main/res/drawable-xhdpi/menu_forward_default.png
index 4212b9df2..9a5b9d85f 100644
Binary files a/app/src/main/res/drawable-xhdpi/menu_forward_default.png and b/app/src/main/res/drawable-xhdpi/menu_forward_default.png differ
diff --git a/app/src/main/res/drawable-xhdpi/menu_imdn_info_default.png b/app/src/main/res/drawable-xhdpi/menu_imdn_info_default.png
deleted file mode 100644
index 1b83a3b9f..000000000
Binary files a/app/src/main/res/drawable-xhdpi/menu_imdn_info_default.png and /dev/null differ
diff --git a/app/src/main/res/drawable-xhdpi/menu_reply_default.png b/app/src/main/res/drawable-xhdpi/menu_reply_default.png
index 47fc5db73..7f898c47e 100644
Binary files a/app/src/main/res/drawable-xhdpi/menu_reply_default.png and b/app/src/main/res/drawable-xhdpi/menu_reply_default.png differ
diff --git a/app/src/main/res/drawable-xhdpi/splashscreen_branding.png b/app/src/main/res/drawable-xhdpi/splashscreen_branding.png
new file mode 100644
index 000000000..99ec7945b
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/splashscreen_branding.png differ
diff --git a/app/src/main/res/drawable/launch_screen.xml b/app/src/main/res/drawable/launch_screen.xml
deleted file mode 100644
index 5a3965dfc..000000000
--- a/app/src/main/res/drawable/launch_screen.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
- -
-
-
- -
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/menu_imdn_info.xml b/app/src/main/res/drawable/menu_imdn_info.xml
index dfa734b2d..d15e39140 100644
--- a/app/src/main/res/drawable/menu_imdn_info.xml
+++ b/app/src/main/res/drawable/menu_imdn_info.xml
@@ -1,7 +1,7 @@
-
-
diff --git a/app/src/main/res/drawable/vector_linphone_logo.xml b/app/src/main/res/drawable/vector_linphone_logo.xml
new file mode 100644
index 000000000..9c1d994f9
--- /dev/null
+++ b/app/src/main/res/drawable/vector_linphone_logo.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout-land/about_fragment.xml b/app/src/main/res/layout-land/about_fragment.xml
index ca69f55be..b57607329 100644
--- a/app/src/main/res/layout-land/about_fragment.xml
+++ b/app/src/main/res/layout-land/about_fragment.xml
@@ -1,6 +1,5 @@
-
+
@@ -13,6 +12,9 @@
+
@@ -164,9 +166,21 @@
android:layout_gravity="center"
android:gravity="center"
android:paddingTop="10dp"
- android:paddingBottom="20dp"
+ android:paddingBottom="10dp"
android:text="@string/about_text" />
+
+
@@ -89,6 +89,7 @@
+ android:src="@{viewModel.autoInitiateVideoCalls ? @drawable/call_video_start : @drawable/call_audio_start, default=@drawable/call_audio_start}" />
+ android:src="@{viewModel.autoInitiateVideoCalls ? @drawable/call_video_start : @drawable/call_audio_start, default=@drawable/call_audio_start}" />
+ android:src="@{viewModel.autoInitiateVideoCalls ? @drawable/call_video_start : @drawable/call_audio_start, default=@drawable/call_audio_start}" />
-
+
@@ -13,6 +12,9 @@
+
@@ -168,9 +170,21 @@
android:layout_gravity="center"
android:gravity="center"
android:paddingTop="10dp"
- android:paddingBottom="20dp"
+ android:paddingBottom="10dp"
android:text="@string/about_text" />
+
+
diff --git a/app/src/main/res/layout/assistant_echo_canceller_calibration_fragment.xml b/app/src/main/res/layout/assistant_echo_canceller_calibration_fragment.xml
index 183386b3e..acf86c157 100644
--- a/app/src/main/res/layout/assistant_echo_canceller_calibration_fragment.xml
+++ b/app/src/main/res/layout/assistant_echo_canceller_calibration_fragment.xml
@@ -16,7 +16,7 @@
android:id="@+id/top_bar_fragment"
android:name="org.linphone.activities.assistant.fragments.TopBarFragment"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="@dimen/main_activity_top_bar_size"
android:layout_alignParentTop="true"
tools:layout="@layout/assistant_top_bar_fragment" />
diff --git a/app/src/main/res/layout/assistant_email_account_creation_fragment.xml b/app/src/main/res/layout/assistant_email_account_creation_fragment.xml
index 78906ee61..03d2f83f0 100644
--- a/app/src/main/res/layout/assistant_email_account_creation_fragment.xml
+++ b/app/src/main/res/layout/assistant_email_account_creation_fragment.xml
@@ -19,7 +19,7 @@
android:id="@+id/top_bar_fragment"
android:name="org.linphone.activities.assistant.fragments.TopBarFragment"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="@dimen/main_activity_top_bar_size"
android:layout_alignParentTop="true"
tools:layout="@layout/assistant_top_bar_fragment" />
diff --git a/app/src/main/res/layout/assistant_email_account_validation_fragment.xml b/app/src/main/res/layout/assistant_email_account_validation_fragment.xml
index 73639b611..6a487897e 100644
--- a/app/src/main/res/layout/assistant_email_account_validation_fragment.xml
+++ b/app/src/main/res/layout/assistant_email_account_validation_fragment.xml
@@ -18,7 +18,7 @@
android:id="@+id/top_bar_fragment"
android:name="org.linphone.activities.assistant.fragments.TopBarFragment"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="@dimen/main_activity_top_bar_size"
android:layout_alignParentTop="true"
tools:layout="@layout/assistant_top_bar_fragment" />
diff --git a/app/src/main/res/layout/assistant_generic_account_login_fragment.xml b/app/src/main/res/layout/assistant_generic_account_login_fragment.xml
index a1790bf5c..42a11a26a 100644
--- a/app/src/main/res/layout/assistant_generic_account_login_fragment.xml
+++ b/app/src/main/res/layout/assistant_generic_account_login_fragment.xml
@@ -20,7 +20,7 @@
android:id="@+id/top_bar_fragment"
android:name="org.linphone.activities.assistant.fragments.TopBarFragment"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="@dimen/main_activity_top_bar_size"
android:layout_alignParentTop="true"
tools:layout="@layout/assistant_top_bar_fragment" />
diff --git a/app/src/main/res/layout/assistant_generic_account_warning_fragment.xml b/app/src/main/res/layout/assistant_generic_account_warning_fragment.xml
new file mode 100644
index 000000000..b4ab8d0c1
--- /dev/null
+++ b/app/src/main/res/layout/assistant_generic_account_warning_fragment.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/assistant_phone_account_creation_fragment.xml b/app/src/main/res/layout/assistant_phone_account_creation_fragment.xml
index 8ae77dcbc..7e3f11c7c 100644
--- a/app/src/main/res/layout/assistant_phone_account_creation_fragment.xml
+++ b/app/src/main/res/layout/assistant_phone_account_creation_fragment.xml
@@ -24,7 +24,7 @@
android:id="@+id/top_bar_fragment"
android:name="org.linphone.activities.assistant.fragments.TopBarFragment"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="@dimen/main_activity_top_bar_size"
android:layout_alignParentTop="true"
tools:layout="@layout/assistant_top_bar_fragment" />
diff --git a/app/src/main/res/layout/assistant_phone_account_linking_fragment.xml b/app/src/main/res/layout/assistant_phone_account_linking_fragment.xml
index 5bc5d3a63..b17358407 100644
--- a/app/src/main/res/layout/assistant_phone_account_linking_fragment.xml
+++ b/app/src/main/res/layout/assistant_phone_account_linking_fragment.xml
@@ -24,7 +24,7 @@
android:id="@+id/top_bar_fragment"
android:name="org.linphone.activities.assistant.fragments.TopBarFragment"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="@dimen/main_activity_top_bar_size"
android:layout_alignParentTop="true"
tools:layout="@layout/assistant_top_bar_fragment" />
diff --git a/app/src/main/res/layout/assistant_phone_account_validation_fragment.xml b/app/src/main/res/layout/assistant_phone_account_validation_fragment.xml
index 2d0aba0d5..e047a3b8e 100644
--- a/app/src/main/res/layout/assistant_phone_account_validation_fragment.xml
+++ b/app/src/main/res/layout/assistant_phone_account_validation_fragment.xml
@@ -18,7 +18,7 @@
android:id="@+id/top_bar_fragment"
android:name="org.linphone.activities.assistant.fragments.TopBarFragment"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="@dimen/main_activity_top_bar_size"
android:layout_alignParentTop="true"
tools:layout="@layout/assistant_top_bar_fragment" />
diff --git a/app/src/main/res/layout/assistant_qr_code_fragment.xml b/app/src/main/res/layout/assistant_qr_code_fragment.xml
index ff0946e12..89c6b82ee 100644
--- a/app/src/main/res/layout/assistant_qr_code_fragment.xml
+++ b/app/src/main/res/layout/assistant_qr_code_fragment.xml
@@ -17,7 +17,7 @@
android:id="@+id/top_bar_fragment"
android:name="org.linphone.activities.assistant.fragments.TopBarFragment"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="@dimen/main_activity_top_bar_size"
android:layout_alignParentTop="true"
tools:layout="@layout/assistant_top_bar_fragment" />
diff --git a/app/src/main/res/layout/assistant_remote_provisioning_fragment.xml b/app/src/main/res/layout/assistant_remote_provisioning_fragment.xml
index 84d818ad6..3cfbfb2f0 100644
--- a/app/src/main/res/layout/assistant_remote_provisioning_fragment.xml
+++ b/app/src/main/res/layout/assistant_remote_provisioning_fragment.xml
@@ -21,7 +21,7 @@
android:id="@+id/top_bar_fragment"
android:name="org.linphone.activities.assistant.fragments.TopBarFragment"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="@dimen/main_activity_top_bar_size"
android:layout_alignParentTop="true"
tools:layout="@layout/assistant_top_bar_fragment" />
diff --git a/app/src/main/res/layout/assistant_welcome_fragment.xml b/app/src/main/res/layout/assistant_welcome_fragment.xml
index 582de4e67..54aa1e66d 100644
--- a/app/src/main/res/layout/assistant_welcome_fragment.xml
+++ b/app/src/main/res/layout/assistant_welcome_fragment.xml
@@ -29,7 +29,7 @@
android:id="@+id/top_bar_fragment"
android:name="org.linphone.activities.assistant.fragments.TopBarFragment"
android:layout_width="match_parent"
- android:layout_height="wrap_content"
+ android:layout_height="@dimen/main_activity_top_bar_size"
android:layout_alignParentTop="true"
tools:layout="@layout/assistant_top_bar_fragment" />
diff --git a/app/src/main/res/layout/chat_bubble_activity.xml b/app/src/main/res/layout/chat_bubble_activity.xml
index 7b9c755d7..8ed3a2130 100644
--- a/app/src/main/res/layout/chat_bubble_activity.xml
+++ b/app/src/main/res/layout/chat_bubble_activity.xml
@@ -97,15 +97,16 @@
android:id="@+id/message"
android:enabled="@{!chatSendingViewModel.isReadOnly}"
android:text="@={chatSendingViewModel.textToSend}"
+ android:imeOptions="@{chatSendingViewModel.imeFlags}"
+ android:hint="@string/chat_room_sending_message_hint"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
- android:layout_marginLeft="5dp"
+ android:layout_marginStart="10dp"
android:layout_weight="1"
android:background="@drawable/resizable_text_field"
- android:imeOptions="flagNoExtractUi"
android:inputType="textShortMessage|textMultiLine|textAutoComplete|textAutoCorrect|textCapSentences"
android:maxLines="6"
android:padding="5dp"
@@ -123,11 +124,11 @@
android:contentDescription="@string/content_description_send_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:padding="5dp"
+ android:padding="10dp"
android:src="@drawable/chat_send_message" />
+ android:paddingRight="5dp"
+ android:text="@{data.text + ' '}"
+ android:textColor="@{data.security || data.groupLeft ? @color/red_color : @color/light_grey_color}"
+ android:textSize="13sp"
+ android:textStyle="italic" />
diff --git a/app/src/main/res/layout/chat_message_list_cell.xml b/app/src/main/res/layout/chat_message_list_cell.xml
index 0d56b3f67..e9736d087 100644
--- a/app/src/main/res/layout/chat_message_list_cell.xml
+++ b/app/src/main/res/layout/chat_message_list_cell.xml
@@ -166,7 +166,7 @@
android:layout_gravity="@{data.chatMessage.outgoing ? Gravity.RIGHT : Gravity.LEFT}"
app:data="@{data.replyData}"
app:clickListener="@{replyClickListener}"
- android:visibility="@{data.chatMessage.reply ? View.VISIBLE : View.GONE, default=gone}" />
+ android:visibility="@{data.replyData != null ? View.VISIBLE : View.GONE, default=gone}" />
+
+ android:layout_height="wrap_content"
+ android:visibility="@{inflatedVisibility}">
+
+ android:paddingBottom="10dp"
+ android:visibility="@{inflatedVisibility}">
+
@@ -152,16 +155,24 @@
android:background="?attr/lightToolbarBackgroundColor"
android:orientation="vertical">
-
+ android:layout="@layout/chat_message_reply"
+ app:cancelClickListener="@{cancelReplyToClickListener}"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
-
+ android:layout="@layout/chat_message_voice_recording"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
-
-
+
+
@@ -48,7 +48,7 @@
android:visibility="@{devicesHidden ? View.GONE : View.VISIBLE}"
android:background="@drawable/menu_background"
android:onClick="@{devicesListener}"
- android:drawableRight="@drawable/menu_security_default"
+ android:drawableRight="@drawable/chat_room_menu_security"
style="@style/popup_item_font"
android:text="@string/chat_room_context_menu_participants_devices" />
diff --git a/app/src/main/res/layout/contact_sync_account_picker_fragment.xml b/app/src/main/res/layout/contact_sync_account_picker_fragment.xml
index ce4ed4c77..cce12b119 100644
--- a/app/src/main/res/layout/contact_sync_account_picker_fragment.xml
+++ b/app/src/main/res/layout/contact_sync_account_picker_fragment.xml
@@ -11,7 +11,8 @@
+ android:orientation="vertical"
+ android:background="?attr/backgroundColor">
+ type="org.linphone.activities.main.history.data.CallLogData" />
-
-
-
-
-
-
-
+ android:layout_height="match_parent"
+ android:layout_below="@id/top_bar">
+ android:orientation="vertical"
+ android:paddingTop="10dp"
+ android:paddingBottom="5dp">
-
+
-
+
-
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
+
+
-
+
-
+
+ type="org.linphone.activities.main.history.data.CallLogData" />
diff --git a/app/src/main/res/layout/launcher_activity.xml b/app/src/main/res/layout/launcher_activity.xml
deleted file mode 100644
index 5efc4c664..000000000
--- a/app/src/main/res/layout/launcher_activity.xml
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/settings_advanced_fragment.xml b/app/src/main/res/layout/settings_advanced_fragment.xml
index 5f95b0ae6..e5fe2c4b7 100644
--- a/app/src/main/res/layout/settings_advanced_fragment.xml
+++ b/app/src/main/res/layout/settings_advanced_fragment.xml
@@ -1,6 +1,7 @@
+ xmlns:linphone="http://schemas.android.com/apk/res-auto"
+ xmlns:bind="http://schemas.android.com/tools">
diff --git a/app/src/main/res/layout/settings_audio_fragment.xml b/app/src/main/res/layout/settings_audio_fragment.xml
index ef34b864e..75fa5a969 100644
--- a/app/src/main/res/layout/settings_audio_fragment.xml
+++ b/app/src/main/res/layout/settings_audio_fragment.xml
@@ -110,6 +110,12 @@
linphone:selectedIndex="@{viewModel.outputAudioDeviceIndex}"
linphone:labels="@{viewModel.outputAudioDeviceLabels}"/>
+
+
+
+
+ linphone:checked="@={viewModel.pauseCallsWhenAudioFocusIsLost}"
+ linphone:enabled="@{!viewModel.useTelecomManager}"/>
diff --git a/app/src/main/res/layout/settings_widget_text.xml b/app/src/main/res/layout/settings_widget_text.xml
index ab33322a4..6a483763a 100644
--- a/app/src/main/res/layout/settings_widget_text.xml
+++ b/app/src/main/res/layout/settings_widget_text.xml
@@ -31,7 +31,7 @@
-
+ android:id="@+id/action_welcomeFragment_to_genericAccountWarningFragment"
+ app:destination="@id/genericAccountWarningFragment" />
@@ -38,6 +38,15 @@
android:id="@+id/action_accountLoginFragment_to_phoneAccountValidationFragment"
app:destination="@id/phoneAccountValidationFragment" />
+
+
+
+
+
Vorname
Firma
Wähle einen Kontakt oder lege einen neuen an
- Soll dieser Kontakt wirklich gelöscht werden\?
-\nEr wird auch aus dem Adressbuch des Gerätes gelöscht
+ Soll dieser Kontakt wirklich gelöscht werden\?\nEr wird auch aus dem Adressbuch des Gerätes gelöscht
Wo soll der Kontakt gespeichert werden
Telefonnummer oder Adresse eingeben
Log starten
@@ -125,8 +124,7 @@
Diesen Eintrag löschen\?
Hallo, benutze &appName;! Du kannst es kostenlos hier %s herunterladen
Kein Schreibzugriff auf Kontakte, Kontakt kann nicht geändert werden
- Soll dieser Kontakt gelöscht werden\?
-\nEr wird auch aus dem Adressbuch des Gerätes gelöscht
+ Soll dieser Kontakt gelöscht werden\?\nEr wird auch aus dem Adressbuch des Gerätes gelöscht
Lokal speichern
Konfigurationsdatei ansehen
Du bist der Gruppe beigetreten
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 44151a411..1f6dea08d 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -63,10 +63,8 @@
Organización
Selecciona un contacto o crea uno nuevo
Permiso de escritura de contactos denegado, no se puede editar el contacto
- Quieres eliminar este contacto\?
-\nTambién se eliminará de la libreta de direcciones de su dispositivo
- Estás seguro de que deseas eliminar estos contactos\?
-\nTambién se eliminarán de la libreta de direcciones de su dispositivo
+ Quieres eliminar este contacto\?\nTambién se eliminará de la libreta de direcciones de su dispositivo
+ Estás seguro de que deseas eliminar estos contactos\?\nTambién se eliminarán de la libreta de direcciones de su dispositivo
Elige dónde guardar tu contacto
Almacenar localmente
Ingrese un número o una dirección
@@ -192,7 +190,7 @@
Contactos
Marcador telefónico
Conversaciones
- Eliminar el último caracter
+ Eliminar el último carácter
Crear contacto
Mostrar todas las llamadas
Mostrar solo llamadas perdidad
@@ -229,16 +227,10 @@
Contacto es seleccionado
Mostrat una lsita de todos los contactos SIP
Adjunte un archivo al mensaje
-
-\nGracias a tu número de teléfono, tus amigos te encontrarán más fácilmente.
-\n
-\n Verás en tu libreta de direcciones quién está usando &appName; y tus amigos sabrán que pueden contactarte en &appName; también.
-\n
+ \nGracias a tu número de teléfono, tus amigos te encontrarán más fácilmente.\n\n Verás en tu libreta de direcciones quién está usando &appName; y tus amigos sabrán que pueden contactarte en &appName; también.\n
Cambiar la duración efímera por el valor seleccionado
El contacto es un administrador en esta conversación
- Sólo puedes usar tu número de teléfono con una cuenta de &appName;.
-\n
-\nSi ya habías vinculado tu número a otra cuenta pero prefieres usar esta, simplemente vincúlala ahora y tu número se moverá automáticamente a esta cuenta.
+ Sólo puedes usar tu número de teléfono con una cuenta de &appName;.\n\nSi ya habías vinculado tu número a otra cuenta pero prefieres usar esta, simplemente vincúlala ahora y tu número se moverá automáticamente a esta cuenta.
El contacto puede ser invitado en conversaciones cifradas
Nombre de usuario ya utilizado
Ya hay una cuenta que utiliza este número
@@ -374,11 +366,7 @@
Buscar la configuración remota
Calibración del cancelador de eco en curso
Para qué se usará mi número de teléfono\?
-
-\nGracias a tu número de teléfono, tus amigos te encontrarán más fácilmente.
-\n
-\n Verás en tu libreta de direcciones quién está usando &appName; y tus amigos sabrán que pueden contactarte en &appName; también.
-\n
+ \nGracias a tu número de teléfono, tus amigos te encontrarán más fácilmente.\n\n Verás en tu libreta de direcciones quién está usando &appName; y tus amigos sabrán que pueden contactarte en &appName; también.\n
Solo digítos
Caracteres inválidos encontrados
Las contraseñas no coinciden
@@ -401,8 +389,7 @@
Su cuenta está creada. Por favor, comprueba tus correos para validar tu cuenta:
Una vez hecho, vuelve aquí y pulsa el botón.
Usar un nombre de usuario (opcional)
- Para completar la verificación de su número de teléfono, por favor, introduzca el código de 4 dígitos a continuación:
-\n
+ Para completar la verificación de su número de teléfono, por favor, introduzca el código de 4 dígitos a continuación:\n
Saltar
Ligar cuenta
Buscar la configuración remota
@@ -603,4 +590,27 @@
Pausar llamadas cuando se pierde el enfoque del audio
Siempre abra archivos dentro de esta aplicación
Aún podrá exportarlos en aplicaciones de terceros
-
\ No newline at end of file
+ Ayúdanos a traducir &appName;
+ Comprendo
+ El mensaje será eliminado
+ Cancelar
+ Dirigir el audio al dispositivo bluetooth si existe
+ El mensaje es una respuesta
+ Archivo adjunto al mensaje
+ Cancelar la respuesta
+ Respuesta
+ Responder
+ Respuesta
+ Mensaje
+ Mantenga pulsado el botón para grabar un mensaje de voz
+ El volumen de los medios de comunicación es bajo, es posible que desee aumentarlo
+
+ - %1$d mensaje no leído
+ - %1$d mensajes no leídos
+
+ Tono de llamada
+ Requiere permisos adicionales
+ Desplazarse al final o al primer mensaje no leído
+ Tendrá prioridad sobre el dispositivo de salida por defecto
+ Mejorar las interacciones con los dispositivos bluetooth
+
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 75a13a517..df0cff122 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -41,7 +41,7 @@
Retirer le contact
&appName; a démarré automatiquement
Contacts &appName;
- Consultez notre politique de confidentialité
+ Consulter notre politique de confidentialité
URL des traces copiée dans le presse-papier
Erreur
Numéro de téléphone
@@ -88,8 +88,7 @@
Prénom
Société
Impossible d\'éditer les contacts car la permission a été refusée.
- Voulez-vous supprimer ces contacts \?
-\nIls seront aussi retirés du carnet d\'adresses du téléphone.
+ Voulez-vous supprimer ces contacts \?\nIls seront aussi retirés du carnet d\'adresses du téléphone.
Choisissez où enregistrer le contact
Enregistrer localement
%s a quitté le groupe
@@ -189,9 +188,7 @@
Télécharger la configuration distante
Calibration de l\'annulateur d\'écho en cours
Comment mon numéro de téléphone sera-t-il utilisé \?
- Vous ne pouvez associer votre numéro qu\'à un seul compte &appName;.
-\n
-\nSi vous avez déjà associé votre numéro à un autre compte mais préférez utiliser ce compte-ci, suivez la procédure d\'association et votre numéro sera automatiquement transféré à ce compte.
+ Vous ne pouvez associer votre numéro qu\'à un seul compte &appName;.\n\nSi vous avez déjà associé votre numéro à un autre compte mais préférez utiliser ce compte-ci, suivez la procédure d\'association et votre numéro sera automatiquement transféré à ce compte.
Entrez uniquement des chiffres
Ce nom d\'utilisateur est déjà utilisé
Ce nom d\'utilisateur est déjà pris
@@ -481,7 +478,7 @@
Tous les appels
Appel entrant
Appel sortant
- Créer une conversation 1-1
+ Créer une conversation 1–1
Créer une conversation de groupe
Non sécurisé
Sécurisé
@@ -512,8 +509,7 @@
- %d jour
- %d jours
- Voulez-vous supprimer ce contact \?
-\nIl sera aussi retiré du carnet d\'adresses du téléphone.
+ Voulez-vous supprimer ce contact \?\nIl sera aussi retiré du carnet d\'adresses du téléphone.
Entrez un numéro ou une adresse
Votre correspondant est en train de taper…
Les clés de sécurité ont changé pour %s
@@ -530,22 +526,13 @@
Résolution vidéo envoyée :
Filtre d\'affichage :
Cet assistant va vous aider à configurer et utiliser votre compte SIP.
-
-\nGrâce à votre numéro de téléphone, vos amis vous trouverons plus facilement.
-\n
-\nVous verrez dans votre carnet d\'adresses qui utilise &appName; et vos amis verront également qu\'ils peuvent vous contacter via &appName;.
-\n
-
-\nVos amis pourront vous joindre plus facilement si vous associez votre compte à votre numéro de téléphone.
-\n
-\nVous verrez dans votre carnet d\'adresses les contacts qui utilisent &appName; et vos amis sauront qu\'ils peuvent vous contacter.
-\n
+ \nGrâce à votre numéro de téléphone, vos amis vous trouverons plus facilement.\n\nVous verrez dans votre carnet d\'adresses qui utilise &appName; et vos amis verront également qu\'ils peuvent vous contacter via &appName;.\n
+ \nVos amis pourront vous joindre plus facilement si vous associez votre compte à votre numéro de téléphone.\n\nVous verrez dans votre carnet d\'adresses les contacts qui utilisent &appName; et vos amis sauront qu\'ils peuvent vous contacter.\n
Le compte n\'existe pas ou les mots de passe ne correspondent pas
Veuillez saisir le nom d\'utilisateur et mot de passe de votre compte &appName;
Nom d\'affichage (optionnel)
Pour valider votre compte, veuillez suivre les instructions reçues par email et cliquer sur le bouton.
- Pour valider votre numéro veuillez enter le code à 4 chiffres ci-dessous :
-\n
+ Pour valider votre numéro veuillez enter le code à 4 chiffres ci-dessous :\n
Envoyer le son vers les écouteurs
Afficher ou masquer le menu de sélection du périphérique audio
<Censuré>
@@ -624,6 +611,17 @@
Autorise l\'écran à être capturé/enregistré sur les vues sensibles
Améliore les intéractions avec les périphériques bluetooth
Nécessite des permissions supplémentaires
- %1$d messages non lus
- %1$d message non lu
+
+ - %1$d message non lu
+ - %1$d messages non lus
+
+ Aller au dernier message reçu ou au premier message non lu
+ Acheminer l\'audio vers l\'appareil bluetooth, s\'il existe
+ Il aura la priorité sur le périphérique de sortie par défaut
+ Sonnerie
+ Le message va être supprimé
+ Annuler
+ Contribuer aux traductions
+ Certaines fonctionnalités avancées comme les messages de groupe ou les messages éphémères nécessitent un compte &appName;.\n\nElles seront masquées dans l\'application si vous configurez un compte SIP tiers.\n\nSi vous souhaitez les activer pour un projet professionnel, contactez-nous.
+ J\'ai compris
\ No newline at end of file
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index b28960821..01937613c 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -76,8 +76,7 @@
%s már nem felügyelő
Nincs beszélgetési előzmény
Névjegy kiválasztása vagy új névjegy létrehozása
- Törli ezt a névjegyet\?
-\nEltávolítja az eszköz címjegyzékéből is
+ Törli ezt a névjegyet\?\nEltávolítja az eszköz címjegyzékéből is
Válassza ki, hová mentse a névjegyet
%s eszköz eltávolítva
A LIME-azonosító kulcs megváltozott %s számára
@@ -164,9 +163,7 @@
Távoli beállítások lekérése
A hívott fél szüneteltette a hívását
Hogyan fog a telefonszámom használódni\?
- Telefonszámát csak egy &appName;-fiókjával használhatja.
-\n
-\nHa már összekapcsolta telefonszámát egy másik fiókkal, de inkább ezt használja, egyszerűen kapcsolja össze most, és a telefonszám önműködően átkerül erre a fiókra.
+ Telefonszámát csak egy &appName;-fiókjával használhatja.\n\nHa már összekapcsolta telefonszámát egy másik fiókkal, de inkább ezt használja, egyszerűen kapcsolja össze most, és a telefonszám önműködően átkerül erre a fiókra.
Ez a felhasználónév már foglalt
Használja a(z) &appName;-fiókját
Elfogadom a Belledonne Communications %1$s és %2$s
@@ -398,8 +395,7 @@
felhasználási feltételeit
adatvédelmi irányelveit
A névjegy írásának engedélye megtagadva, a névjegy nem szerkeszthető
- Biztosan törli ezeket a névjegyeket\?
-\nEzeket a rendszer eltávolítja az eszköz címjegyzékéből is
+ Biztosan törli ezeket a névjegyeket\?\nEzeket a rendszer eltávolítja az eszköz címjegyzékéből is
Az azonnali üzenetek végpontok közötti titkosításra kerülnek a biztonságos beszélgetések során. Lehetőség van a résztvevők hitelesítésével frissíteni a beszélgetés biztonsági szintjét. Ehhez hívja a partneret, és kövesse a hitelesítési folyamatot.
Elmúló üzenetek lejárati dátuma: %s
Ez az üzenet mindkét félnél törlődik, miután elolvasta és a kiválasztott időt túllépte.
@@ -409,16 +405,8 @@
Végpontok közötti titkosítás engedélyezve
Kattintson a visszhangkioltás beállításának megkezdéséhez
Kattintson a visszhang ellenőrzés indításához
-
-\nTelefonszámának köszönhetően ismerősei könnyebben megtalálják Önt.
-\n
-\nA címjegyzékében látni fogja, hogy ki használja a(z) &appName; és az ismerősök tudni fogják, hogy elérhetik Önt a(z) &appName; is.
-\n
-
-\nAz ismerősei könnyebben megtalálja Önt, ha összekapcsolja fiókját a telefonszámával.
-\n
-\nA címjegyzékében látni fogja, hogy ki használja a(z) &appName; és az ismerősök tudni fogják, hogy elérhetik Önt a(z) &appName; is.
-\n
+ \nTelefonszámának köszönhetően ismerősei könnyebben megtalálják Önt.\n\nA címjegyzékében látni fogja, hogy ki használja a(z) &appName; és az ismerősök tudni fogják, hogy elérhetik Önt a(z) &appName; is.\n
+ \nAz ismerősei könnyebben megtalálja Önt, ha összekapcsolja fiókját a telefonszámával.\n\nA címjegyzékében látni fogja, hogy ki használja a(z) &appName; és az ismerősök tudni fogják, hogy elérhetik Önt a(z) &appName; is.\n
Ismerőslista feliratkozása
Kérjük, adja meg a(z) &appName;-fiókjának felhasználónevét és jelszavát
Fiókját még nem érvényesítettük
@@ -458,8 +446,7 @@
Közepes hívásminőség
Nyissa meg a fájlt külső alkalmazásokban
Fiókja létrejött. Kérjük, ellenőrizze e-mailjeit fiókja érvényesítéséhez:
- A telefonszám hitelesítésének befejezéséhez kérjük, hogy adja meg a négyjegyű kódot lent:
-\n
+ A telefonszám hitelesítésének befejezéséhez kérjük, hogy adja meg a négyjegyű kódot lent:\n
A kamera előnézetének megjelenítése a tárcsázón
A hívás önműködően elindul, ha egy másik alkalmazásból indul
Ha a megadott maximális méretnél könnyebb
@@ -580,11 +567,11 @@
Egy kiszolgálót fog használni a feltöltéshez és egy másikat a letöltéshez
Meghatalmazás-konfiguráció nem törli a regisztrációt
Nemzetközi előhívószám alkalmazása a kimenő hívásokhoz és csevegéshez
- &appName; azonnali üzenetek értesítései
+ &appName; azonnali üzenetek értesítései
Szeretné megpróbálni megnyitni egyszerű szöveges fájlként\?
Engedélyezze vagy tiltsa le a hívás rögzítését
Folytassa a jelenleg tartott konferenciát
- Az ismerős egy &appName; felhasználó
+ Az ismerős egy &appName; felhasználó
A kijelölt elemek törlése a listából
9
Híváselőzmények
@@ -624,6 +611,17 @@
Képernyőfelvétel engedélyezése bizalmas nézetekben
További engedélyeket igényel
Tevékenységek fejlesztése Bluetooth-eszközökkel
- %1$d olvasatlan üzenet
- %1$d olvasatlan üzenet
+
+ - %1$d olvasatlan üzenet
+ - %1$d olvasatlan üzenet
+
+ Ennek elsőbbsége lesz az alapértelmezett kimeneti eszközzel szemben
+ Görgetés legalulra vagy az első olvasatlan üzenetre
+ Hang átirányítása a Bluetooth-eszközre (ha van ilyen)
+ Csengőhang
+ Megszakítás
+ Üzenet törlésre kerül
+ Segítsen nekünk a &appName; fordításában
+ Megértettem
+ Egyes szolgáltatásokhoz egy &appName; fiók kellene, például csoportos üzeneteket vagy elmúló üzeneteket.\n\nEzek a szolgáltatások el vannak rejtve, ha külső SIP-fiókkal regisztrál.\n\nHa engedélyezni szeretné egy kereskedelmi projektben, vegye fel velünk a kapcsolatot.
\ No newline at end of file
diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml
index 6933329f9..83cce0c6d 100644
--- a/app/src/main/res/values-night/styles.xml
+++ b/app/src/main/res/values-night/styles.xml
@@ -4,7 +4,7 @@
+
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 4fc92bf15..6b2460429 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -1,9 +1,7 @@
-
+
]>
-
传输
UDP 用户数据报协议
@@ -375,10 +373,8 @@
机构
选择一个联系人或创建一个新联系人
写入联系人权限被拒绝,无法编辑联系人
- 您要删除这个联系人吗?
-\n它也将会从您的设备通讯录中删除
- 您确定要删除这些联系人吗?
-\n他们将会从您的设备通讯录中删除
+ 您要删除这个联系人吗?\n它也将会从您的设备通讯录中删除
+ 您确定要删除这些联系人吗?\n他们将会从您的设备通讯录中删除
- \@string/history_delete_one_dialog
@@ -477,9 +473,7 @@
欢迎
创建帐户
正在进行回音消除校正
- 您的电话号码只能关联一个&appName;账户。
-\n
-\n如果您已经把您的号码关联了其他账户,但是您更想使用这个。只需关联您现在的账户,你的号码就会自动转移到这个账户。
+ 您的电话号码只能关联一个&appName;账户。\n\n如果您已经把您的号码关联了其他账户,但是您更想使用这个。只需关联您现在的账户,你的号码就会自动转移到这个账户。
只能输入数字
用户名已被使用
已有账户关联这个电话号码
@@ -515,8 +509,7 @@
一旦完成,请返回并点击此处。
使用一个用户名(可选)
我们已向您的手机发送了验证码短信:
- 请输入4位数字码以完成验证:
-\n
+ 请输入4位数字码以完成验证:\n
您将用您的电话号码关联以下用户名:
跳过
关联账户
@@ -547,16 +540,8 @@
对方要求启用视讯
显示筛选器:
-
-\n益于您的电话号码,您的朋友们可以更容易的找到您。
-\n
-\n您在通讯录里可以查看谁在使用&appName;,他们也同样可以通过&appName;联系您。
-\n
-
-\n如果将您的帐户关联您的电话号码,您的朋友们可以更容易的找到你。
-\n
-\n您在通讯录里可以查看谁在使用&appName;,他们也同样可以通过&appName;联系您。
-\n
+ \n益于您的电话号码,您的朋友们可以更容易的找到您。\n\n您在通讯录里可以查看谁在使用&appName;,他们也同样可以通过&appName;联系您。\n
+ \n如果将您的帐户关联您的电话号码,您的朋友们可以更容易的找到你。\n\n您在通讯录里可以查看谁在使用&appName;,他们也同样可以通过&appName;联系您。\n
请输入您的用户名与密码和您的SIP域名
您的帐户已经创建。请检查邮件以驗證您的帐户:
接收视频分辨率:
@@ -569,4 +554,81 @@
显示名
用户名
启用短暂消息(测试版)
-
+ 上传日志失败
+ 查看配置文件
+ 回复
+ 选择或创建对话以转发消息
+ 似乎我们无法显示该文件。
+ 您想以文本形式打开它还是将其(未加密)导出到第三方应用程序(如果有)?
+ 服务器超时
+ 主账户
+ 改善与蓝牙设备的交互
+ 将拒绝的呼叫重定向到语音邮件 URI
+ 始终在此应用内打开文件
+ 您仍然可以在第三方应用程序中导出它们
+ 加密一切
+ 允许对敏感视图进行屏幕捕获/录制
+ DTLS
+ 如果输入了数字,则将前缀应用于数字
+ 留言即回复
+ 录制音频信息
+ 截取收到的视频截图
+ 在应用程序中打开对话而不是气泡
+ 日志已清除
+ 回复
+ 选择或创建对话以共享文件
+ 文件未找到
+ 没有可用于此类文件的应用
+ 无法在聊天气泡中打开加密文件
+ 选择或创建对话以共享文本
+ 导出
+ 作为文本打开
+ 回复
+ 信息
+ 按住按钮录制语音信息
+ 您的媒体音量很低,您可能需要提高音量
+
+ - %1$d 未读消息
+ - %1$d 未读消息
+
+ 暂时不可用
+ 错误:%s
+ 使用条款
+ 隐私政策
+ 我接受 Belledonne Communications 的 %1$s 和 %2$s
+ 需要一些额外的权限
+ 通话时全屏应用
+ 隐藏状态栏和导航栏
+ 在应用程序外显示叠加层
+ 音频焦点丢失时暂停通话
+ 自动开始通话录音
+ 在传入的早期媒体流期间响铃
+ 自动下载传入的录音
+ 一旦启用就无法禁用!
+ 调试设置
+ 其他设置
+ 禁用 UI 的安全模式
+ &appName; 未接来电通知
+ 应用前缀于拨出的电话和聊天
+ 在此对话中转发消息
+ 附加到消息的文件
+ 关闭通知气泡
+ 在第三方应用程序中打开文件
+ 取消消息转发
+ 取消分享
+ 取消回复
+ 录制语音留言
+ 停止录音
+ 取消录音
+ 暂停录音
+ 播放录音
+ 帮助我们翻译 &appName;
+ 某些功能需要 1&appName; 帐户,例如群组消息或临时消息。\n\n当您注册第三方 SIP 帐户时,这些功能会被隐藏。\n\n要在商业项目中启用它,请联系我们。
+ 将音频转到蓝牙设备(如果有)
+ 它将优先于默认输出设备
+ 滚动到底部或第一条未读消息
+ 铃声
+ 消息将被删除
+ 中止
+ 我明白
+
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index e4979f265..e1bf7eaa8 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -56,8 +56,7 @@
名字
機構
寫入聯絡人權限被拒,無法編輯聯絡人
- 您要刪除這個聯絡人嗎?
-\n它也將會從您的設備通訊錄中刪除
+ 您要刪除這個聯絡人嗎?\n它也將會從您的設備通訊錄中刪除
選擇保存聯絡人的位置
本地存儲
輸入電話號碼或地址
@@ -93,8 +92,7 @@
通話紀錄中沒有通話
通訊錄中沒有SIP聯絡人。
選擇聯絡人或建立新的聯絡人
- 您要刪除這些聯絡人嗎?
-\n它們也將會從您的設備通訊錄中刪除
+ 您要刪除這些聯絡人嗎?\n它們也將會從您的設備通訊錄中刪除
檢測到中間人攻擊
該信息已閱讀後,兩端的信息將在選定的超時時間被刪除。
通話被拒
@@ -180,11 +178,7 @@
獲取遠程配置
正在進行回音消除校正
我的電話號碼將用於?
- "
-\n如果將您的帳戶鏈接您的電話號碼,您的朋友們可以更容易的找到你。
-\n
-\n您在通訊錄裡可以查看誰在使用&appName ;,他們也同樣可以通過&appName;聯繫您。
-\n"
+ "\n如果將您的帳戶鏈接您的電話號碼,您的朋友們可以更容易的找到你。\n\n您在通訊錄裡可以查看誰在使用&appName ;,他們也同樣可以通過&appName;聯繫您。\n"
只能輸入數字
用戶名已被使用
已有帳戶鏈接這個號碼
@@ -208,8 +202,7 @@
您的帳戶已經創建。請檢查郵件以驗證您的帳戶:
一旦完成,請返回並點擊此處。
使用一個用戶名(可選)
- 請輸入4位數字碼以完成驗證:
-\n
+ 請輸入4位數字碼以完成驗證:\n
您將用您的電話號碼關聯以下用戶名:
跳過
鏈接帳戶
@@ -218,14 +211,8 @@
URL網址
不明的URL網址格式,無法下載配置資源…
助手將會幫助您配置和使用您的SIP帳戶。
-
-\n益於您的電話號碼,您的朋友們將更容易找到您。
-\n
-\n您在通訊錄裡可以查看誰在使用&appName;,他們也同樣可以通過&appName;聯繫您。
-\n
- 您的電話號碼只能鏈接一個&appName;賬戶。
-\n
-\n如果您已經把您的號碼鏈接了其他帳戶,但是您更想使用這個。只需鏈接您現在的賬戶,你的號碼就會自動轉移到這個賬戶。
+ \n益於您的電話號碼,您的朋友們將更容易找到您。\n\n您在通訊錄裡可以查看誰在使用&appName;,他們也同樣可以通過&appName;聯繫您。\n
+ 您的電話號碼只能鏈接一個&appName;賬戶。\n\n如果您已經把您的號碼鏈接了其他帳戶,但是您更想使用這個。只需鏈接您現在的賬戶,你的號碼就會自動轉移到這個賬戶。
您的帳戶尚未經驗證
帳戶不存在或密碼不匹配
繼續
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a3879da35..d1782128e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -4,7 +4,7 @@
]>
-
+
&appName;
&appName; (debug)
&appName;Android
@@ -20,7 +20,9 @@
&appName; Android %s
&appName; SDK %s
Visit our privacy policy
- GNU General Public License v3.0\n © 2010–2021 Belledonne Communications
+ GNU General Public License v3.0\n © 2010–2022 Belledonne Communications
+ https://weblate.linphone.org/projects/linphone/
+ Help us translate &appName;
Failed to upload logs!
@@ -221,12 +223,12 @@
Message
Hold button to record voice message
Your media volume is low, you may want to increase it
- %1$d unread message
- %1$d unread messages
- - @string/chat_room_unread_message
- - @string/chat_room_unread_messages
+ - %1$d unread message
+ - %1$d unread messages
+ Message will be deleted
+ Abort
No recordings
@@ -289,6 +291,9 @@
terms of use
privacy policy
I accept Belledonne Communications\' %1$s and %2$s
+ Some features require a &appName; account, such as group messaging or ephemeral messaging.\n\nThese features are hidden when you register with a third party SIP account.\n\nTo enable it in a commercial project, please contact us.
+ https://www.linphone.org/contact
+ I understand
Only digits are expected here
@@ -376,6 +381,8 @@
Changes will take effect starting next call
Default output audio device
Changes will take effect starting next call
+ Route audio to the bluetooth device if any
+ It will have priority over the default output device
Codec bitrate limit
Microphone gain
(in decibels)
@@ -400,6 +407,7 @@
Use device ringtone
+ Ringtone
Vibrate while incoming call is ringing
Media encryption
None
@@ -737,4 +745,5 @@
Cancel voice recording
Pause voice recording
Play voice recording
+ Scroll to bottom or first unread message
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 68a0c9e5c..276db38de 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -1,11 +1,17 @@
+
+