Merge branch 'release/4.6'

This commit is contained in:
Sylvain Berfini 2022-02-09 15:05:35 +01:00
commit b5c141f081
201 changed files with 4953 additions and 2698 deletions

1
.gitignore vendored
View file

@ -24,5 +24,6 @@ linphone-sdk-android/*.aar
app/debug app/debug
app/release app/release
app/releaseAppBundle app/releaseAppBundle
app/releaseWithCrashlytics
keystore.properties keystore.properties
app/src/main/res/xml/contacts.xml app/src/main/res/xml/contacts.xml

View file

@ -7,6 +7,7 @@ job-android:
before_script: before_script:
- if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then eval $(ssh-agent -s); fi - 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 - 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: script:
- sdkmanager - sdkmanager

View file

@ -10,33 +10,58 @@ Group changes to describe their impact on the project, as follows:
Fixed for any bug fixes. Fixed for any bug fixes.
Security to invite users to upgrade in case of vulnerabilities. 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 ### Added
- Reply to chat message feature (with original message preview) - Reply to chat message feature (with original message preview)
- Swipe action on chat messages to reply / delete
- Voice recordings in chat feature - Voice recordings in chat feature
- Allow video recording in chat file sharing - Allow video recording in chat file sharing
- Unread messages indicator in chat conversation that separates read & unread messages - 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 - New video call UI on foldable device like Galaxy Z Fold
- Setting to automatically record all calls - 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 ### Changed
- UI has been reworked around SlidingPane component to better handle tablets & foldable devices - 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 - 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 - 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) - 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 - Bumped dependencies, gradle updated from 4.2.2 to 7.0.2
- Target Android SDK version set to 31 (Android 12) - Target Android SDK version set to 31 (Android 12)
- Splashscreen is using new APIs
- SDK updated to 5.1.0 release - SDK updated to 5.1.0 release
- Updated translations
### Fixed ### Fixed
- Chat notifications disappearing when app restarts - Chat notifications disappearing when app restarts
- "Infinite backstack", now each view is stored (at most) once in the backstack - "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 - 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 ### 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 - 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 ## [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 - Fixed various crashes & other issues
- SDK bumped to 5.0.10 - SDK bumped to 5.0.10
## [4.5.1] - Unreleased ## [4.5.1] - 2021-07-15
### Added
- Reply to chat message feature
- Voice recordings messages
### Changed ### Changed
- Navigation was reworked using SlidingPane widget, reducing code & improving UI on foldables - Bugs & crashes have been fixed
- SDK bumped to 5.0.1
### Removed
## [4.5.0] - 2021-07-08 ## [4.5.0] - 2021-07-08

View file

@ -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. - 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: 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. 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 ## 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: 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. 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. 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.

View file

@ -5,6 +5,9 @@ plugins {
id 'org.jlleitschuh.gradle.ktlint' id 'org.jlleitschuh.gradle.ktlint'
} }
def appVersionName = "4.6.0"
def appVersionCode = 40600 // 4.06.00
static def getPackageName() { static def getPackageName() {
return "org.linphone" return "org.linphone"
} }
@ -24,7 +27,7 @@ if (crashlyticsEnabled) {
def gitBranch = new ByteArrayOutputStream() def gitBranch = new ByteArrayOutputStream()
task getGitVersion() { task getGitVersion() {
def gitVersion = "4.7.0" def gitVersion = appVersionName
def gitVersionStream = new ByteArrayOutputStream() def gitVersionStream = new ByteArrayOutputStream()
def gitCommitsCount = new ByteArrayOutputStream() def gitCommitsCount = new ByteArrayOutputStream()
def gitCommitHash = new ByteArrayOutputStream() def gitCommitHash = new ByteArrayOutputStream()
@ -52,9 +55,9 @@ task getGitVersion() {
} else { } else {
gitVersion = gitVersionStream.toString().trim() + "." + gitCommitsCount.toString().trim() + "+" + gitCommitHash.toString().trim() gitVersion = gitVersionStream.toString().trim() + "." + gitCommitsCount.toString().trim() + "+" + gitCommitHash.toString().trim()
} }
println("Git version: " + gitVersion) println("Git version: " + gitVersion + " (" + appVersionCode + ")")
} catch (ignored) { } catch (ignored) {
println("Git not found") println("Git not found, using " + gitVersion + " (" + appVersionCode + ")")
} }
project.version = gitVersion project.version = gitVersion
} }
@ -84,7 +87,7 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 31 targetSdkVersion 31
versionCode 4700 versionCode appVersionCode
versionName "${project.version}" versionName "${project.version}"
applicationId getPackageName() 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 // 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", variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
linphone_file_provider: getPackageName() + ".fileprovider", linphone_file_provider: getPackageName() + ".fileprovider",
appLabel: "@string/app_name", appLabel: "@string/app_name",
@ -143,8 +146,8 @@ android {
initWith release initWith release
resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString() resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString()
if (crashlyticsEnabled) {
if (crashlyticsEnabled) {
firebaseCrashlytics { firebaseCrashlytics {
nativeSymbolUploadEnabled true nativeSymbolUploadEnabled true
unstrippedNativeLibsDir file(LinphoneSdkBuildDir + '/libs-debug/').toString() unstrippedNativeLibsDir file(LinphoneSdkBuildDir + '/libs-debug/').toString()
@ -161,14 +164,13 @@ android {
resValue "string", "sync_account_type", getPackageName() + ".sync" resValue "string", "sync_account_type", getPackageName() + ".sync"
resValue "string", "file_provider", getPackageName() + ".debug.fileprovider" resValue "string", "file_provider", getPackageName() + ".debug.fileprovider"
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address" resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"
resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString()
if (!firebaseEnabled) { if (!firebaseEnabled) {
resValue "string", "gcm_defaultSenderId", "none" resValue "string", "gcm_defaultSenderId", "none"
} }
resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString()
if (crashlyticsEnabled) { if (crashlyticsEnabled) {
firebaseCrashlytics { firebaseCrashlytics {
nativeSymbolUploadEnabled true nativeSymbolUploadEnabled true
unstrippedNativeLibsDir file(LinphoneSdkBuildDir + '/libs-debug/').toString() 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 { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.media:media:1.4.3' 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' 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-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version" implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0-beta01" implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
implementation "androidx.window:window:1.0.0-beta03" 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-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "androidx.security:security-crypto-ktx:1.1.0-alpha03" 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 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'androidx.emoji:emoji:1.1.0' implementation 'androidx.emoji:emoji:1.1.0'

View file

@ -42,25 +42,21 @@
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
android:allowNativeHeapPointerTagging="false"> android:allowNativeHeapPointerTagging="false">
<activity <activity android:name=".activities.main.MainActivity"
android:name=".activities.launcher.LauncherActivity"
android:exported="true" android:exported="true"
android:noHistory="true" android:launchMode="singleTask"
android:theme="@style/AppTheme"> android:windowSoftInputMode="adjustResize"
android:theme="@style/AppSplashScreenTheme">
<nav-graph android:value="@navigation/main_nav_graph" />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<meta-data <meta-data
android:name="android.app.shortcuts" android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" /> android:resource="@xml/shortcuts" />
</activity>
<activity android:name=".activities.main.MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<nav-graph android:value="@navigation/main_nav_graph" />
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW_LOCUS" /> <action android:name="android.intent.action.VIEW_LOCUS" />

View file

@ -16,6 +16,7 @@
<entry name="realm" overwrite="true"></entry> <entry name="realm" overwrite="true"></entry>
<entry name="conference_factory_uri" overwrite="true"></entry> <entry name="conference_factory_uri" overwrite="true"></entry>
<entry name="push_notification_allowed" overwrite="true">0</entry> <entry name="push_notification_allowed" overwrite="true">0</entry>
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">0</entry>
</section> </section>
<section name="nat_policy_default_values"> <section name="nat_policy_default_values">
<entry name="stun_server" overwrite="true"></entry> <entry name="stun_server" overwrite="true"></entry>

View file

@ -16,6 +16,7 @@
<entry name="realm" overwrite="true">sip.linphone.org</entry> <entry name="realm" overwrite="true">sip.linphone.org</entry>
<entry name="conference_factory_uri" overwrite="true">sip:conference-factory@sip.linphone.org</entry> <entry name="conference_factory_uri" overwrite="true">sip:conference-factory@sip.linphone.org</entry>
<entry name="push_notification_allowed" overwrite="true">1</entry> <entry name="push_notification_allowed" overwrite="true">1</entry>
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">1</entry>
</section> </section>
<section name="nat_policy_default_values"> <section name="nat_policy_default_values">
<entry name="stun_server" overwrite="true">stun.linphone.org</entry> <entry name="stun_server" overwrite="true">stun.linphone.org</entry>

View file

@ -25,13 +25,12 @@ import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.Display import android.view.Display
import android.view.Surface
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.ActivityNavigator import androidx.navigation.ActivityNavigator
import androidx.window.layout.FoldingFeature import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoRepository.Companion.windowInfoRepository import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo import androidx.window.layout.WindowLayoutInfo
import java.util.* import java.util.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -58,9 +57,12 @@ abstract class GenericActivity : AppCompatActivity() {
ensureCoreExists(applicationContext) ensureCoreExists(applicationContext)
lifecycleScope.launch(Dispatchers.Main) { lifecycleScope.launch(Dispatchers.Main) {
windowInfoRepository().windowLayoutInfo.collect { newLayoutInfo -> WindowInfoTracker
updateCurrentLayout(newLayoutInfo) .getOrCreate(this@GenericActivity)
} .windowLayoutInfo(this@GenericActivity)
.collect { newLayoutInfo ->
updateCurrentLayout(newLayoutInfo)
}
} }
requestedOrientation = if (corePreferences.forcePortrait) { requestedOrientation = if (corePreferences.forcePortrait) {
@ -97,18 +99,6 @@ abstract class GenericActivity : AppCompatActivity() {
override fun onResume() { override fun onResume() {
super.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 // Remove service notification if it has been started by device boot
coreContext.notificationsManager.stopForegroundNotificationIfPossible() coreContext.notificationsManager.stopForegroundNotificationIfPossible()
} }

View file

@ -28,8 +28,12 @@ import androidx.core.view.doOnPreDraw
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.transition.MaterialSharedAxis 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.LinphoneApplication.Companion.corePreferences
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
@ -38,9 +42,17 @@ abstract class GenericFragment<T : ViewDataBinding> : Fragment() {
protected val binding get() = _binding!! protected val binding get() = _binding!!
protected var useMaterialSharedAxisXForwardAnimation = true protected var useMaterialSharedAxisXForwardAnimation = true
protected fun isBindingAvailable(): Boolean {
return _binding != null
}
protected val onBackPressedCallback = object : OnBackPressedCallback(true) { protected val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
goBack() lifecycleScope.launch {
withContext(Dispatchers.Main) {
goBack()
}
}
} }
} }

View file

@ -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 */ /* Tabs fragment related */
internal fun TabsFragment.navigateToCallHistory() { internal fun TabsFragment.navigateToCallHistory() {
@ -79,9 +103,8 @@ internal fun TabsFragment.navigateToCallHistory() {
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_masterCallLogsFragment R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_masterCallLogsFragment
R.id.dialerFragment -> R.id.action_dialerFragment_to_masterCallLogsFragment R.id.dialerFragment -> R.id.action_dialerFragment_to_masterCallLogsFragment
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_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( findNavController().navigate(
action, action,
null, null,
@ -94,9 +117,8 @@ internal fun TabsFragment.navigateToContacts() {
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_masterContactsFragment R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_masterContactsFragment
R.id.dialerFragment -> R.id.action_dialerFragment_to_masterContactsFragment R.id.dialerFragment -> R.id.action_dialerFragment_to_masterContactsFragment
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_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( findNavController().navigate(
action, action,
null, null,
@ -109,9 +131,8 @@ internal fun TabsFragment.navigateToDialer() {
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_dialerFragment R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_dialerFragment
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_dialerFragment R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_dialerFragment
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_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( findNavController().navigate(
action, action,
null, null,
@ -124,9 +145,8 @@ internal fun TabsFragment.navigateToChatRooms() {
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_masterChatRoomsFragment R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_masterChatRoomsFragment
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_masterChatRoomsFragment R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_masterChatRoomsFragment
R.id.dialerFragment -> R.id.action_dialerFragment_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( findNavController().navigate(
action, action,
null, null,
@ -298,7 +318,15 @@ internal fun DetailChatRoomFragment.navigateToEmptyChatRoom() {
findNavController().navigate( findNavController().navigate(
R.id.action_global_emptyChatFragment, R.id.action_global_emptyChatFragment,
null, 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( findNavController().navigate(
R.id.action_chatRoomCreationFragment_to_detailChatRoomFragment, R.id.action_chatRoomCreationFragment_to_detailChatRoomFragment,
args, args,
popupTo(R.id.detailChatRoomFragment, true) popupTo(R.id.chatRoomCreationFragment, true)
) )
} }
} }
@ -326,7 +354,7 @@ internal fun ChatRoomCreationFragment.navigateToEmptyChatRoom() {
findNavController().navigate( findNavController().navigate(
R.id.action_global_emptyChatFragment, R.id.action_global_emptyChatFragment,
null, 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) { if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
findNavController().navigate( findNavController().navigate(
R.id.action_welcomeFragment_to_genericAccountLoginFragment, R.id.action_welcomeFragment_to_genericAccountWarningFragment,
null, null,
popupTo() 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() { internal fun GenericAccountLoginFragment.navigateToEchoCancellerCalibration() {
if (findNavController().currentDestination?.id == R.id.genericAccountLoginFragment) { if (findNavController().currentDestination?.id == R.id.genericAccountLoginFragment) {
findNavController().navigate( findNavController().navigate(

View file

@ -21,5 +21,6 @@ package org.linphone.activities
interface SnackBarActivity { interface SnackBarActivity {
fun showSnackBar(resourceId: Int) fun showSnackBar(resourceId: Int)
fun showSnackBar(resourceId: Int, action: Int, listener: () -> Unit)
fun showSnackBar(message: String) fun showSnackBar(message: String)
} }

View file

@ -23,6 +23,7 @@ import android.os.Bundle
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
import org.linphone.activities.GenericActivity import org.linphone.activities.GenericActivity
import org.linphone.activities.SnackBarActivity import org.linphone.activities.SnackBarActivity
@ -40,12 +41,23 @@ class AssistantActivity : GenericActivity(), SnackBarActivity {
sharedViewModel = ViewModelProvider(this)[SharedAssistantViewModel::class.java] sharedViewModel = ViewModelProvider(this)[SharedAssistantViewModel::class.java]
coordinator = findViewById(R.id.coordinator) coordinator = findViewById(R.id.coordinator)
corePreferences.firstStart = false
} }
override fun showSnackBar(resourceId: Int) { override fun showSnackBar(resourceId: Int) {
Snackbar.make(coordinator, resourceId, Snackbar.LENGTH_LONG).show() 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) { override fun showSnackBar(message: String) {
Snackbar.make(coordinator, message, Snackbar.LENGTH_LONG).show() Snackbar.make(coordinator, message, Snackbar.LENGTH_LONG).show()
} }

View file

@ -20,6 +20,7 @@
package org.linphone.activities.assistant.fragments package org.linphone.activities.assistant.fragments
import android.annotation.TargetApi
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.databinding.ViewDataBinding import androidx.databinding.ViewDataBinding
import com.google.android.material.dialog.MaterialAlertDialogBuilder 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.activities.assistant.viewmodels.AbstractPhoneViewModel
import org.linphone.compatibility.Compatibility import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.mediastream.Version
import org.linphone.utils.PermissionHelper import org.linphone.utils.PermissionHelper
import org.linphone.utils.PhoneNumberUtils import org.linphone.utils.PhoneNumberUtils
abstract class AbstractPhoneFragment<T : ViewDataBinding> : GenericFragment<T>() { abstract class AbstractPhoneFragment<T : ViewDataBinding> : GenericFragment<T>() {
companion object {
const val READ_PHONE_STATE_PERMISSION_REQUEST_CODE = 0
}
abstract val viewModel: AbstractPhoneViewModel abstract val viewModel: AbstractPhoneViewModel
override fun onRequestPermissionsResult( override fun onRequestPermissionsResult(
@ -39,7 +45,7 @@ abstract class AbstractPhoneFragment<T : ViewDataBinding> : GenericFragment<T>()
permissions: Array<out String>, permissions: Array<out String>,
grantResults: IntArray grantResults: IntArray
) { ) {
if (requestCode == 0) { if (requestCode == READ_PHONE_STATE_PERMISSION_REQUEST_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Log.i("[Assistant] READ_PHONE_STATE/READ_PHONE_NUMBERS permission granted") Log.i("[Assistant] READ_PHONE_STATE/READ_PHONE_NUMBERS permission granted")
updateFromDeviceInfo() updateFromDeviceInfo()
@ -49,11 +55,12 @@ abstract class AbstractPhoneFragment<T : ViewDataBinding> : GenericFragment<T>()
} }
} }
protected fun checkPermission() { @TargetApi(Version.API23_MARSHMALLOW_60)
protected fun checkPermissions() {
if (!resources.getBoolean(R.bool.isTablet)) { if (!resources.getBoolean(R.bool.isTablet)) {
if (!PermissionHelper.get().hasReadPhoneStateOrPhoneNumbersPermission()) { if (!PermissionHelper.get().hasReadPhoneStateOrPhoneNumbersPermission()) {
Log.i("[Assistant] Asking for READ_PHONE_STATE/READ_PHONE_NUMBERS permission") 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 { } else {
updateFromDeviceInfo() updateFromDeviceInfo()
} }

View file

@ -35,6 +35,7 @@ import org.linphone.activities.main.viewmodels.DialogViewModel
import org.linphone.activities.navigateToEchoCancellerCalibration import org.linphone.activities.navigateToEchoCancellerCalibration
import org.linphone.activities.navigateToPhoneAccountValidation import org.linphone.activities.navigateToPhoneAccountValidation
import org.linphone.databinding.AssistantAccountLoginFragmentBinding import org.linphone.databinding.AssistantAccountLoginFragmentBinding
import org.linphone.mediastream.Version
import org.linphone.utils.DialogUtils import org.linphone.utils.DialogUtils
class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragmentBinding>() { class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragmentBinding>() {
@ -52,7 +53,10 @@ class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragment
ViewModelProvider(this)[SharedAssistantViewModel::class.java] ViewModelProvider(this)[SharedAssistantViewModel::class.java]
} }
viewModel = ViewModelProvider(this, AccountLoginViewModelFactory(sharedViewModel.getAccountCreator()))[AccountLoginViewModel::class.java] viewModel = ViewModelProvider(
this,
AccountLoginViewModelFactory(sharedViewModel.getAccountCreator())
)[AccountLoginViewModel::class.java]
binding.viewModel = viewModel binding.viewModel = viewModel
if (resources.getBoolean(R.bool.isTablet)) { if (resources.getBoolean(R.bool.isTablet)) {
@ -75,66 +79,65 @@ class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragment
} }
viewModel.goToSmsValidationEvent.observe( viewModel.goToSmsValidationEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
val args = Bundle() val args = Bundle()
args.putBoolean("IsLogin", true) args.putBoolean("IsLogin", true)
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber) args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
navigateToPhoneAccountValidation(args) navigateToPhoneAccountValidation(args)
}
} }
) }
viewModel.leaveAssistantEvent.observe( viewModel.leaveAssistantEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
coreContext.contactsManager.updateLocalContacts() coreContext.contactsManager.updateLocalContacts()
if (coreContext.core.isEchoCancellerCalibrationRequired) { if (coreContext.core.isEchoCancellerCalibrationRequired) {
navigateToEchoCancellerCalibration() navigateToEchoCancellerCalibration()
} else { } else {
requireActivity().finish() requireActivity().finish()
}
} }
} }
) }
viewModel.invalidCredentialsEvent.observe( viewModel.invalidCredentialsEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
val dialogViewModel = DialogViewModel(getString(R.string.assistant_error_invalid_credentials)) val dialogViewModel =
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel) DialogViewModel(getString(R.string.assistant_error_invalid_credentials))
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
dialogViewModel.showCancelButton { dialogViewModel.showCancelButton {
viewModel.removeInvalidProxyConfig() viewModel.removeInvalidProxyConfig()
dialog.dismiss() dialog.dismiss()
}
dialogViewModel.showDeleteButton(
{
viewModel.continueEvenIfInvalidCredentials()
dialog.dismiss()
},
getString(R.string.assistant_continue_even_if_credentials_invalid)
)
dialog.show()
} }
dialogViewModel.showDeleteButton(
{
viewModel.continueEvenIfInvalidCredentials()
dialog.dismiss()
},
getString(R.string.assistant_continue_even_if_credentials_invalid)
)
dialog.show()
} }
) }
viewModel.onErrorEvent.observe( viewModel.onErrorEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { message -> it.consume { message ->
(requireActivity() as AssistantActivity).showSnackBar(message) (requireActivity() as AssistantActivity).showSnackBar(message)
}
} }
) }
checkPermission() if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
checkPermissions()
}
} }
} }

View file

@ -31,6 +31,10 @@ import org.linphone.databinding.AssistantEchoCancellerCalibrationFragmentBinding
import org.linphone.utils.PermissionHelper import org.linphone.utils.PermissionHelper
class EchoCancellerCalibrationFragment : GenericFragment<AssistantEchoCancellerCalibrationFragmentBinding>() { class EchoCancellerCalibrationFragment : GenericFragment<AssistantEchoCancellerCalibrationFragmentBinding>() {
companion object {
const val RECORD_AUDIO_PERMISSION_REQUEST_CODE = 0
}
private lateinit var viewModel: EchoCancellerCalibrationViewModel private lateinit var viewModel: EchoCancellerCalibrationViewModel
override fun getLayoutId(): Int = R.layout.assistant_echo_canceller_calibration_fragment override fun getLayoutId(): Int = R.layout.assistant_echo_canceller_calibration_fragment
@ -44,17 +48,16 @@ class EchoCancellerCalibrationFragment : GenericFragment<AssistantEchoCancellerC
binding.viewModel = viewModel binding.viewModel = viewModel
viewModel.echoCalibrationTerminated.observe( viewModel.echoCalibrationTerminated.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
requireActivity().finish() requireActivity().finish()
}
} }
) }
if (!PermissionHelper.required(requireContext()).hasRecordAudioPermission()) { if (!PermissionHelper.required(requireContext()).hasRecordAudioPermission()) {
Log.i("[Echo Canceller Calibration] Asking for RECORD_AUDIO permission") Log.i("[Echo Canceller Calibration] Asking for RECORD_AUDIO permission")
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 0) requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), RECORD_AUDIO_PERMISSION_REQUEST_CODE)
} else { } else {
viewModel.startEchoCancellerCalibration() viewModel.startEchoCancellerCalibration()
} }
@ -65,13 +68,16 @@ class EchoCancellerCalibrationFragment : GenericFragment<AssistantEchoCancellerC
permissions: Array<out String>, permissions: Array<out String>,
grantResults: IntArray grantResults: IntArray
) { ) {
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED if (requestCode == RECORD_AUDIO_PERMISSION_REQUEST_CODE) {
if (granted) { val granted =
Log.i("[Echo Canceller Calibration] RECORD_AUDIO permission granted") grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
viewModel.startEchoCancellerCalibration() if (granted) {
} else { Log.i("[Echo Canceller Calibration] RECORD_AUDIO permission granted")
Log.w("[Echo Canceller Calibration] RECORD_AUDIO permission denied") viewModel.startEchoCancellerCalibration()
requireActivity().finish() } else {
Log.w("[Echo Canceller Calibration] RECORD_AUDIO permission denied")
requireActivity().finish()
}
} }
} }
} }

View file

@ -50,21 +50,19 @@ class EmailAccountCreationFragment : GenericFragment<AssistantEmailAccountCreati
binding.viewModel = viewModel binding.viewModel = viewModel
viewModel.goToEmailValidationEvent.observe( viewModel.goToEmailValidationEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
navigateToEmailAccountValidation() navigateToEmailAccountValidation()
}
} }
) }
viewModel.onErrorEvent.observe( viewModel.onErrorEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { message -> it.consume { message ->
(requireActivity() as AssistantActivity).showSnackBar(message) (requireActivity() as AssistantActivity).showSnackBar(message)
}
} }
) }
} }
} }

View file

@ -49,27 +49,25 @@ class EmailAccountValidationFragment : GenericFragment<AssistantEmailAccountVali
binding.viewModel = viewModel binding.viewModel = viewModel
viewModel.leaveAssistantEvent.observe( viewModel.leaveAssistantEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
coreContext.contactsManager.updateLocalContacts() coreContext.contactsManager.updateLocalContacts()
val args = Bundle() val args = Bundle()
args.putBoolean("AllowSkip", true) args.putBoolean("AllowSkip", true)
args.putString("Username", viewModel.accountCreator.username) args.putString("Username", viewModel.accountCreator.username)
args.putString("Password", viewModel.accountCreator.password) args.putString("Password", viewModel.accountCreator.password)
navigateToAccountLinking(args) navigateToAccountLinking(args)
}
} }
) }
viewModel.onErrorEvent.observe( viewModel.onErrorEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { message -> it.consume { message ->
(requireActivity() as AssistantActivity).showSnackBar(message) (requireActivity() as AssistantActivity).showSnackBar(message)
}
} }
) }
} }
} }

View file

@ -54,52 +54,50 @@ class GenericAccountLoginFragment : GenericFragment<AssistantGenericAccountLogin
binding.viewModel = viewModel binding.viewModel = viewModel
viewModel.leaveAssistantEvent.observe( viewModel.leaveAssistantEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
coreContext.contactsManager.updateLocalContacts() coreContext.contactsManager.updateLocalContacts()
if (coreContext.core.isEchoCancellerCalibrationRequired) { if (coreContext.core.isEchoCancellerCalibrationRequired) {
navigateToEchoCancellerCalibration() navigateToEchoCancellerCalibration()
} else { } else {
requireActivity().finish() requireActivity().finish()
}
} }
} }
) }
viewModel.invalidCredentialsEvent.observe( viewModel.invalidCredentialsEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
val dialogViewModel = DialogViewModel(getString(R.string.assistant_error_invalid_credentials)) val dialogViewModel =
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel) DialogViewModel(getString(R.string.assistant_error_invalid_credentials))
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
dialogViewModel.showCancelButton { dialogViewModel.showCancelButton {
viewModel.removeInvalidProxyConfig() viewModel.removeInvalidProxyConfig()
dialog.dismiss() dialog.dismiss()
}
dialogViewModel.showDeleteButton(
{
viewModel.continueEvenIfInvalidCredentials()
dialog.dismiss()
},
getString(R.string.assistant_continue_even_if_credentials_invalid)
)
dialog.show()
} }
dialogViewModel.showDeleteButton(
{
viewModel.continueEvenIfInvalidCredentials()
dialog.dismiss()
},
getString(R.string.assistant_continue_even_if_credentials_invalid)
)
dialog.show()
} }
) }
viewModel.onErrorEvent.observe( viewModel.onErrorEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { message -> it.consume { message ->
(requireActivity() as AssistantActivity).showSnackBar(message) (requireActivity() as AssistantActivity).showSnackBar(message)
}
} }
) }
} }
} }

View file

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

View file

@ -29,8 +29,10 @@ import org.linphone.activities.assistant.viewmodels.PhoneAccountCreationViewMode
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
import org.linphone.activities.navigateToPhoneAccountValidation import org.linphone.activities.navigateToPhoneAccountValidation
import org.linphone.databinding.AssistantPhoneAccountCreationFragmentBinding import org.linphone.databinding.AssistantPhoneAccountCreationFragmentBinding
import org.linphone.mediastream.Version
class PhoneAccountCreationFragment : AbstractPhoneFragment<AssistantPhoneAccountCreationFragmentBinding>() { class PhoneAccountCreationFragment :
AbstractPhoneFragment<AssistantPhoneAccountCreationFragmentBinding>() {
private lateinit var sharedViewModel: SharedAssistantViewModel private lateinit var sharedViewModel: SharedAssistantViewModel
override lateinit var viewModel: PhoneAccountCreationViewModel override lateinit var viewModel: PhoneAccountCreationViewModel
@ -45,7 +47,10 @@ class PhoneAccountCreationFragment : AbstractPhoneFragment<AssistantPhoneAccount
ViewModelProvider(this)[SharedAssistantViewModel::class.java] ViewModelProvider(this)[SharedAssistantViewModel::class.java]
} }
viewModel = ViewModelProvider(this, PhoneAccountCreationViewModelFactory(sharedViewModel.getAccountCreator()))[PhoneAccountCreationViewModel::class.java] viewModel = ViewModelProvider(
this,
PhoneAccountCreationViewModelFactory(sharedViewModel.getAccountCreator())
)[PhoneAccountCreationViewModel::class.java]
binding.viewModel = viewModel binding.viewModel = viewModel
binding.setInfoClickListener { binding.setInfoClickListener {
@ -57,26 +62,26 @@ class PhoneAccountCreationFragment : AbstractPhoneFragment<AssistantPhoneAccount
} }
viewModel.goToSmsValidationEvent.observe( viewModel.goToSmsValidationEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
val args = Bundle() val args = Bundle()
args.putBoolean("IsCreation", true) args.putBoolean("IsCreation", true)
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber) args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
navigateToPhoneAccountValidation(args) navigateToPhoneAccountValidation(args)
}
} }
) }
viewModel.onErrorEvent.observe( viewModel.onErrorEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { message -> it.consume { message ->
(requireActivity() as AssistantActivity).showSnackBar(message) (requireActivity() as AssistantActivity).showSnackBar(message)
}
} }
) }
checkPermission() if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
checkPermissions()
}
} }
} }

View file

@ -30,6 +30,7 @@ import org.linphone.activities.navigateToEchoCancellerCalibration
import org.linphone.activities.navigateToPhoneAccountValidation import org.linphone.activities.navigateToPhoneAccountValidation
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantPhoneAccountLinkingFragmentBinding import org.linphone.databinding.AssistantPhoneAccountLinkingFragmentBinding
import org.linphone.mediastream.Version
class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountLinkingFragmentBinding>() { class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountLinkingFragmentBinding>() {
private lateinit var sharedViewModel: SharedAssistantViewModel private lateinit var sharedViewModel: SharedAssistantViewModel
@ -72,39 +73,38 @@ class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountL
} }
viewModel.goToSmsValidationEvent.observe( viewModel.goToSmsValidationEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
val args = Bundle() val args = Bundle()
args.putBoolean("IsLinking", true) args.putBoolean("IsLinking", true)
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber) args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
navigateToPhoneAccountValidation(args) navigateToPhoneAccountValidation(args)
}
} }
) }
viewModel.leaveAssistantEvent.observe( viewModel.leaveAssistantEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
if (LinphoneApplication.coreContext.core.isEchoCancellerCalibrationRequired) { if (LinphoneApplication.coreContext.core.isEchoCancellerCalibrationRequired) {
navigateToEchoCancellerCalibration() navigateToEchoCancellerCalibration()
} else { } else {
requireActivity().finish() requireActivity().finish()
}
} }
} }
) }
viewModel.onErrorEvent.observe( viewModel.onErrorEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { message -> it.consume { message ->
(requireActivity() as AssistantActivity).showSnackBar(message) (requireActivity() as AssistantActivity).showSnackBar(message)
}
} }
) }
checkPermission() if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
checkPermissions()
}
} }
} }

View file

@ -59,37 +59,38 @@ class PhoneAccountValidationFragment : GenericFragment<AssistantPhoneAccountVali
viewModel.isLinking.value = arguments?.getBoolean("IsLinking", false) viewModel.isLinking.value = arguments?.getBoolean("IsLinking", false)
viewModel.leaveAssistantEvent.observe( viewModel.leaveAssistantEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
when { when {
viewModel.isLogin.value == true || viewModel.isCreation.value == true -> { viewModel.isLogin.value == true || viewModel.isCreation.value == true -> {
coreContext.contactsManager.updateLocalContacts() coreContext.contactsManager.updateLocalContacts()
if (coreContext.core.isEchoCancellerCalibrationRequired) { if (coreContext.core.isEchoCancellerCalibrationRequired) {
navigateToEchoCancellerCalibration() navigateToEchoCancellerCalibration()
} else { } else {
requireActivity().finish() requireActivity().finish()
}
}
viewModel.isLinking.value == true -> {
val args = Bundle()
args.putString("Identity", "sip:${viewModel.accountCreator.username}@${viewModel.accountCreator.domain}")
navigateToAccountSettings(args)
} }
} }
viewModel.isLinking.value == true -> {
val args = Bundle()
args.putString(
"Identity",
"sip:${viewModel.accountCreator.username}@${viewModel.accountCreator.domain}"
)
navigateToAccountSettings(args)
}
} }
} }
) }
viewModel.onErrorEvent.observe( viewModel.onErrorEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { message -> it.consume { message ->
(requireActivity() as AssistantActivity).showSnackBar(message) (requireActivity() as AssistantActivity).showSnackBar(message)
}
} }
) }
val clipboard = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager val clipboard = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
clipboard.addPrimaryClipChangedListener { clipboard.addPrimaryClipChangedListener {

View file

@ -34,6 +34,10 @@ import org.linphone.databinding.AssistantQrCodeFragmentBinding
import org.linphone.utils.PermissionHelper import org.linphone.utils.PermissionHelper
class QrCodeFragment : GenericFragment<AssistantQrCodeFragmentBinding>() { class QrCodeFragment : GenericFragment<AssistantQrCodeFragmentBinding>() {
companion object {
const val CAMERA_PERMISSION_REQUEST_CODE = 0
}
private lateinit var sharedViewModel: SharedAssistantViewModel private lateinit var sharedViewModel: SharedAssistantViewModel
private lateinit var viewModel: QrCodeViewModel private lateinit var viewModel: QrCodeViewModel
@ -52,19 +56,18 @@ class QrCodeFragment : GenericFragment<AssistantQrCodeFragmentBinding>() {
binding.viewModel = viewModel binding.viewModel = viewModel
viewModel.qrCodeFoundEvent.observe( viewModel.qrCodeFoundEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { url -> it.consume { url ->
sharedViewModel.remoteProvisioningUrl.value = url sharedViewModel.remoteProvisioningUrl.value = url
findNavController().navigateUp() findNavController().navigateUp()
}
} }
) }
viewModel.setBackCamera() viewModel.setBackCamera()
if (!PermissionHelper.required(requireContext()).hasCameraPermission()) { if (!PermissionHelper.required(requireContext()).hasCameraPermission()) {
Log.i("[QR Code] Asking for CAMERA permission") 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<AssistantQrCodeFragmentBinding>() {
super.onResume() super.onResume()
coreContext.core.nativePreviewWindowId = binding.qrCodeCaptureTexture coreContext.core.nativePreviewWindowId = binding.qrCodeCaptureTexture
coreContext.core.enableQrcodeVideoPreview(true) coreContext.core.isQrcodeVideoPreviewEnabled = true
coreContext.core.enableVideoPreview(true) coreContext.core.isVideoPreviewEnabled = true
} }
override fun onPause() { override fun onPause() {
coreContext.core.nativePreviewWindowId = null coreContext.core.nativePreviewWindowId = null
coreContext.core.enableQrcodeVideoPreview(false) coreContext.core.isQrcodeVideoPreviewEnabled = false
coreContext.core.enableVideoPreview(false) coreContext.core.isVideoPreviewEnabled = false
super.onPause() super.onPause()
} }
@ -89,14 +92,17 @@ class QrCodeFragment : GenericFragment<AssistantQrCodeFragmentBinding>() {
permissions: Array<out String>, permissions: Array<out String>,
grantResults: IntArray grantResults: IntArray
) { ) {
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
if (granted) { val granted =
Log.i("[QR Code] CAMERA permission granted") grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
coreContext.core.reloadVideoDevices() if (granted) {
viewModel.setBackCamera() Log.i("[QR Code] CAMERA permission granted")
} else { coreContext.core.reloadVideoDevices()
Log.w("[QR Code] CAMERA permission denied") viewModel.setBackCamera()
findNavController().navigateUp() } else {
Log.w("[QR Code] CAMERA permission denied")
findNavController().navigateUp()
}
} }
} }
} }

View file

@ -55,22 +55,21 @@ class RemoteProvisioningFragment : GenericFragment<AssistantRemoteProvisioningFr
} }
viewModel.fetchSuccessfulEvent.observe( viewModel.fetchSuccessfulEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { success -> it.consume { success ->
if (success) { if (success) {
if (coreContext.core.isEchoCancellerCalibrationRequired) { if (coreContext.core.isEchoCancellerCalibrationRequired) {
navigateToEchoCancellerCalibration() navigateToEchoCancellerCalibration()
} else {
requireActivity().finish()
}
} else { } else {
val activity = requireActivity() as AssistantActivity requireActivity().finish()
activity.showSnackBar(R.string.assistant_remote_provisioning_failure)
} }
} else {
val activity = requireActivity() as AssistantActivity
activity.showSnackBar(R.string.assistant_remote_provisioning_failure)
} }
} }
) }
viewModel.urlToFetch.value = sharedViewModel.remoteProvisioningUrl.value ?: coreContext.core.provisioningUri viewModel.urlToFetch.value = sharedViewModel.remoteProvisioningUrl.value ?: coreContext.core.provisioningUri
} }

View file

@ -35,7 +35,6 @@ import org.linphone.activities.*
import org.linphone.activities.assistant.viewmodels.WelcomeViewModel import org.linphone.activities.assistant.viewmodels.WelcomeViewModel
import org.linphone.activities.navigateToAccountLogin import org.linphone.activities.navigateToAccountLogin
import org.linphone.activities.navigateToEmailAccountCreation import org.linphone.activities.navigateToEmailAccountCreation
import org.linphone.activities.navigateToGenericLogin
import org.linphone.activities.navigateToRemoteProvisioning import org.linphone.activities.navigateToRemoteProvisioning
import org.linphone.databinding.AssistantWelcomeFragmentBinding import org.linphone.databinding.AssistantWelcomeFragmentBinding
@ -65,7 +64,7 @@ class WelcomeFragment : GenericFragment<AssistantWelcomeFragmentBinding>() {
} }
binding.setGenericAccountLoginClickListener { binding.setGenericAccountLoginClickListener {
navigateToGenericLogin() navigateToGenericLoginWarning()
} }
binding.setRemoteProvisioningClickListener { binding.setRemoteProvisioningClickListener {
@ -73,11 +72,10 @@ class WelcomeFragment : GenericFragment<AssistantWelcomeFragmentBinding>() {
} }
viewModel.termsAndPrivacyAccepted.observe( viewModel.termsAndPrivacyAccepted.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
if (it) corePreferences.readAndAgreeTermsAndPrivacy = true if (it) corePreferences.readAndAgreeTermsAndPrivacy = true
} }
)
setUpTermsAndPrivacyLinks() setUpTermsAndPrivacyLinks()
} }

View file

@ -56,14 +56,19 @@ abstract class AbstractPhoneViewModel(val accountCreator: AccountCreator) :
} }
fun updateFromPhoneNumberAndOrDialPlan(number: String?, dialPlan: DialPlan?) { fun updateFromPhoneNumberAndOrDialPlan(number: String?, dialPlan: DialPlan?) {
val internationalPrefix = "+${dialPlan?.countryCallingCode}"
if (dialPlan != null) { if (dialPlan != null) {
Log.i("[Assistant] Found prefix from dial plan: ${dialPlan.countryCallingCode}") Log.i("[Assistant] Found prefix from dial plan: ${dialPlan.countryCallingCode}")
prefix.value = "+${dialPlan.countryCallingCode}" prefix.value = internationalPrefix
} }
if (number != null) { if (number != null) {
Log.i("[Assistant] Found phone number: $number") Log.i("[Assistant] Found phone number: $number")
phoneNumber.value = number!! phoneNumber.value = if (number.startsWith(internationalPrefix)) {
number.substring(internationalPrefix.length)
} else {
number
}
} }
} }

View file

@ -25,6 +25,7 @@ import org.linphone.R
import org.linphone.core.* import org.linphone.core.*
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.utils.Event import org.linphone.utils.Event
import org.linphone.utils.PhoneNumberUtils
class AccountLoginViewModelFactory(private val accountCreator: AccountCreator) : class AccountLoginViewModelFactory(private val accountCreator: AccountCreator) :
ViewModelProvider.NewInstanceFactory() { ViewModelProvider.NewInstanceFactory() {
@ -220,6 +221,18 @@ class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewM
proxyConfig.isPushNotificationAllowed = true 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") Log.i("[Assistant] [Account Login] Proxy config created")
return true return true
} }

View file

@ -22,11 +22,13 @@ package org.linphone.activities.assistant.viewmodels
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import org.linphone.LinphoneApplication
import org.linphone.core.AccountCreator import org.linphone.core.AccountCreator
import org.linphone.core.AccountCreatorListenerStub import org.linphone.core.AccountCreatorListenerStub
import org.linphone.core.ProxyConfig import org.linphone.core.ProxyConfig
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.utils.Event import org.linphone.utils.Event
import org.linphone.utils.PhoneNumberUtils
class EmailAccountValidationViewModelFactory(private val accountCreator: AccountCreator) : class EmailAccountValidationViewModelFactory(private val accountCreator: AccountCreator) :
ViewModelProvider.NewInstanceFactory() { ViewModelProvider.NewInstanceFactory() {
@ -106,6 +108,18 @@ class EmailAccountValidationViewModel(val accountCreator: AccountCreator) : View
proxyConfig.isPushNotificationAllowed = true 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") Log.i("[Assistant] [Account Validation] Proxy config created")
return true return true
} }

View file

@ -136,6 +136,7 @@ class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewMo
Log.e("[Assistant] [Generic Login] Account creator couldn't create proxy config") Log.e("[Assistant] [Generic Login] Account creator couldn't create proxy config")
coreContext.core.removeListener(coreListener) coreContext.core.removeListener(coreListener)
onErrorEvent.value = Event("Error: Failed to create account object") onErrorEvent.value = Event("Error: Failed to create account object")
waitForServerAnswer.value = false
return return
} }

View file

@ -61,40 +61,36 @@ class CallActivity : ProximitySensorActivity() {
sharedViewModel = ViewModelProvider(this)[SharedCallViewModel::class.java] sharedViewModel = ViewModelProvider(this)[SharedCallViewModel::class.java]
sharedViewModel.toggleDrawerEvent.observe( sharedViewModel.toggleDrawerEvent.observe(
this, this
{ ) {
it.consume { it.consume {
if (binding.statsMenu.isDrawerOpen(Gravity.LEFT)) { if (binding.statsMenu.isDrawerOpen(Gravity.LEFT)) {
binding.statsMenu.closeDrawer(binding.sideMenuContent, true) binding.statsMenu.closeDrawer(binding.sideMenuContent, true)
} else { } else {
binding.statsMenu.openDrawer(binding.sideMenuContent, true) binding.statsMenu.openDrawer(binding.sideMenuContent, true)
}
} }
} }
) }
sharedViewModel.resetHiddenInterfaceTimerInVideoCallEvent.observe( sharedViewModel.resetHiddenInterfaceTimerInVideoCallEvent.observe(
this, this
{ ) {
it.consume { it.consume {
viewModel.showMomentarily() viewModel.showMomentarily()
}
} }
) }
viewModel.proximitySensorEnabled.observe( viewModel.proximitySensorEnabled.observe(
this, this
{ ) {
enableProximitySensor(it) enableProximitySensor(it)
} }
)
viewModel.videoEnabled.observe( viewModel.videoEnabled.observe(
this, this
{ ) {
updateConstraintSetDependingOnFoldingState() updateConstraintSetDependingOnFoldingState()
} }
)
} }
override fun onLayoutChanges(foldingFeature: FoldingFeature?) { override fun onLayoutChanges(foldingFeature: FoldingFeature?) {

View file

@ -79,24 +79,22 @@ class IncomingCallActivity : GenericActivity() {
binding.viewModel = viewModel binding.viewModel = viewModel
viewModel.callEndedEvent.observe( viewModel.callEndedEvent.observe(
this, this
{ ) {
it.consume { it.consume {
Log.i("[Incoming Call Activity] Call ended, finish activity") Log.i("[Incoming Call Activity] Call ended, finish activity")
finish() finish()
}
} }
) }
viewModel.earlyMediaVideoEnabled.observe( viewModel.earlyMediaVideoEnabled.observe(
this, this
{ ) {
if (it) { if (it) {
Log.i("[Incoming Call Activity] Early media video being received, set native window id") Log.i("[Incoming Call Activity] Early media video being received, set native window id")
coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface
}
} }
) }
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
val keyguardLocked = keyguardManager.isKeyguardLocked val keyguardLocked = keyguardManager.isKeyguardLocked
@ -139,7 +137,7 @@ class IncomingCallActivity : GenericActivity() {
permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO) 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") Log.i("[Incoming Call Activity] Asking for CAMERA permission")
permissionsRequiredList.add(Manifest.permission.CAMERA) permissionsRequiredList.add(Manifest.permission.CAMERA)
} }

View file

@ -80,55 +80,58 @@ class OutgoingCallActivity : ProximitySensorActivity() {
binding.controlsViewModel = controlsViewModel binding.controlsViewModel = controlsViewModel
viewModel.callEndedEvent.observe( viewModel.callEndedEvent.observe(
this, this
{ ) {
it.consume { it.consume {
Log.i("[Outgoing Call Activity] Call ended, finish activity") Log.i("[Outgoing Call Activity] Call ended, finish activity")
finish() finish()
}
} }
) }
viewModel.callConnectedEvent.observe( viewModel.callConnectedEvent.observe(
this, this
{ ) {
it.consume { it.consume {
Log.i("[Outgoing Call Activity] Call connected, finish activity") Log.i("[Outgoing Call Activity] Call connected, finish activity")
finish() finish()
}
} }
) }
controlsViewModel.isSpeakerSelected.observe( controlsViewModel.isSpeakerSelected.observe(
this, this
{ ) {
enableProximitySensor(!it) enableProximitySensor(!it)
} }
)
controlsViewModel.askPermissionEvent.observe( controlsViewModel.askAudioRecordPermissionEvent.observe(
this, this
{ ) {
it.consume { permission -> it.consume { permission ->
requestPermissions(arrayOf(permission), 0) requestPermissions(arrayOf(permission), 0)
}
} }
) }
controlsViewModel.askCameraPermissionEvent.observe(
this
) {
it.consume { permission ->
requestPermissions(arrayOf(permission), 0)
}
}
controlsViewModel.toggleNumpadEvent.observe( controlsViewModel.toggleNumpadEvent.observe(
this, this
{ ) {
it.consume { open -> it.consume { open ->
if (this::numpadAnimator.isInitialized) { if (this::numpadAnimator.isInitialized) {
if (open) { if (open) {
numpadAnimator.start() numpadAnimator.start()
} else { } else {
numpadAnimator.reverse() numpadAnimator.reverse()
}
} }
} }
} }
) }
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) { if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
checkPermissions() checkPermissions()
@ -170,7 +173,7 @@ class OutgoingCallActivity : ProximitySensorActivity() {
Log.i("[Outgoing Call Activity] Asking for RECORD_AUDIO permission") Log.i("[Outgoing Call Activity] Asking for RECORD_AUDIO permission")
permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO) 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") Log.i("[Outgoing Call Activity] Asking for CAMERA permission")
permissionsRequiredList.add(Manifest.permission.CAMERA) permissionsRequiredList.add(Manifest.permission.CAMERA)
} }
@ -207,7 +210,8 @@ class OutgoingCallActivity : ProximitySensorActivity() {
for (call in coreContext.core.calls) { for (call in coreContext.core.calls) {
if (call.state == Call.State.OutgoingInit || if (call.state == Call.State.OutgoingInit ||
call.state == Call.State.OutgoingProgress || call.state == Call.State.OutgoingProgress ||
call.state == Call.State.OutgoingRinging call.state == Call.State.OutgoingRinging ||
call.state == Call.State.OutgoingEarlyMedia
) { ) {
return call return call
} }

View file

@ -36,7 +36,7 @@ class CallStatisticsData(val call: Call) : GenericContactData(call.remoteAddress
private val listener = object : CoreListenerStub() { private val listener = object : CoreListenerStub() {
override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) { override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) {
if (call == this@CallStatisticsData.call) { if (call == this@CallStatisticsData.call) {
isVideoEnabled.value = call.currentParams.videoEnabled() isVideoEnabled.value = call.currentParams.isVideoEnabled
updateCallStats(stats) updateCallStats(stats)
} }
} }
@ -50,7 +50,7 @@ class CallStatisticsData(val call: Call) : GenericContactData(call.remoteAddress
initCallStats() initCallStats()
val videoEnabled = call.currentParams.videoEnabled() val videoEnabled = call.currentParams.isVideoEnabled
isVideoEnabled.value = videoEnabled isVideoEnabled.value = videoEnabled
isExpanded.value = coreContext.core.currentCall == call isExpanded.value = coreContext.core.currentCall == call

View file

@ -88,130 +88,129 @@ class ControlsFragment : GenericFragment<CallControlsFragmentBinding>() {
binding.conferenceViewModel = conferenceViewModel binding.conferenceViewModel = conferenceViewModel
callsViewModel.currentCallViewModel.observe( callsViewModel.currentCallViewModel.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
if (it != null) { if (it != null) {
binding.activeCallTimer.base = binding.activeCallTimer.base =
SystemClock.elapsedRealtime() - (1000 * it.call.duration) // Linphone timestamps are in seconds SystemClock.elapsedRealtime() - (1000 * it.call.duration) // Linphone timestamps are in seconds
binding.activeCallTimer.start() binding.activeCallTimer.start()
}
} }
) }
callsViewModel.noMoreCallEvent.observe( callsViewModel.noMoreCallEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
requireActivity().finish() requireActivity().finish()
}
} }
) }
callsViewModel.askWriteExternalStoragePermissionEvent.observe( callsViewModel.askWriteExternalStoragePermissionEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
if (!PermissionHelper.get().hasWriteExternalStoragePermission()) { if (!PermissionHelper.get().hasWriteExternalStoragePermission()) {
Log.i("[Controls Fragment] Asking for WRITE_EXTERNAL_STORAGE permission") Log.i("[Controls Fragment] Asking for WRITE_EXTERNAL_STORAGE permission")
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1) requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 2)
}
} }
} }
) }
callsViewModel.callUpdateEvent.observe( callsViewModel.callUpdateEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { call -> it.consume { call ->
if (call.state == Call.State.StreamsRunning) { if (call.state == Call.State.StreamsRunning) {
dialog?.dismiss() dialog?.dismiss()
} else if (call.state == Call.State.UpdatedByRemote) { } else if (call.state == Call.State.UpdatedByRemote) {
if (coreContext.core.videoCaptureEnabled() || coreContext.core.videoDisplayEnabled()) { if (coreContext.core.isVideoCaptureEnabled || coreContext.core.isVideoDisplayEnabled) {
if (call.currentParams.videoEnabled() != call.remoteParams?.videoEnabled()) { if (call.currentParams.isVideoEnabled != call.remoteParams?.isVideoEnabled) {
showCallVideoUpdateDialog(call) showCallVideoUpdateDialog(call)
}
} else {
Log.w("[Controls Fragment] Video display & capture are disabled, don't show video dialog")
} }
} else {
Log.w("[Controls Fragment] Video display & capture are disabled, don't show video dialog")
} }
} }
} }
) }
controlsViewModel.chatClickedEvent.observe( controlsViewModel.chatClickedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
val intent = Intent() val intent = Intent()
intent.setClass(requireContext(), MainActivity::class.java) intent.setClass(requireContext(), MainActivity::class.java)
intent.putExtra("Chat", true) intent.putExtra("Chat", true)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent) startActivity(intent)
}
} }
) }
controlsViewModel.addCallClickedEvent.observe( controlsViewModel.addCallClickedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
val intent = Intent() val intent = Intent()
intent.setClass(requireContext(), MainActivity::class.java) intent.setClass(requireContext(), MainActivity::class.java)
intent.putExtra("Dialer", true) intent.putExtra("Dialer", true)
intent.putExtra("Transfer", false) intent.putExtra("Transfer", false)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent) startActivity(intent)
}
} }
) }
controlsViewModel.transferCallClickedEvent.observe( controlsViewModel.transferCallClickedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
val intent = Intent() val intent = Intent()
intent.setClass(requireContext(), MainActivity::class.java) intent.setClass(requireContext(), MainActivity::class.java)
intent.putExtra("Dialer", true) intent.putExtra("Dialer", true)
intent.putExtra("Transfer", true) intent.putExtra("Transfer", true)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent) startActivity(intent)
}
} }
) }
controlsViewModel.askPermissionEvent.observe( controlsViewModel.askAudioRecordPermissionEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { permission -> it.consume { permission ->
Log.i("[Controls Fragment] Asking for $permission permission") Log.i("[Controls Fragment] Asking for $permission permission")
requestPermissions(arrayOf(permission), 0) 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( controlsViewModel.toggleNumpadEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { open -> it.consume { open ->
if (this::numpadAnimator.isInitialized) { if (this::numpadAnimator.isInitialized) {
if (open) { if (open) {
numpadAnimator.start() numpadAnimator.start()
} else { } else {
numpadAnimator.reverse() numpadAnimator.reverse()
}
} }
} }
} }
) }
controlsViewModel.somethingClickedEvent.observe( controlsViewModel.somethingClickedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
sharedViewModel.resetHiddenInterfaceTimerInVideoCallEvent.value = Event(true) sharedViewModel.resetHiddenInterfaceTimerInVideoCallEvent.value = Event(true)
}
} }
) }
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) { if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
checkPermissions() checkPermissions()
@ -251,7 +250,13 @@ class ControlsFragment : GenericFragment<CallControlsFragmentBinding>() {
} }
} }
} }
} 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() callsViewModel.takeScreenshot()
} }
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)

View file

@ -64,15 +64,14 @@ class StatusFragment : GenericFragment<CallStatusFragmentBinding>() {
} }
viewModel.showZrtpDialogEvent.observe( viewModel.showZrtpDialogEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { call -> it.consume { call ->
if (call.state == Call.State.Connected || call.state == Call.State.StreamsRunning) { if (call.state == Call.State.Connected || call.state == Call.State.StreamsRunning) {
showZrtpDialog(call) showZrtpDialog(call)
}
} }
} }
) }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -114,8 +113,8 @@ class StatusFragment : GenericFragment<CallStatusFragmentBinding>() {
val viewModel = DialogViewModel(getString(R.string.zrtp_dialog_message), getString(R.string.zrtp_dialog_title)) val viewModel = DialogViewModel(getString(R.string.zrtp_dialog_message), getString(R.string.zrtp_dialog_title))
viewModel.showZrtp = true viewModel.showZrtp = true
viewModel.zrtpReadSas = toRead.toUpperCase(Locale.getDefault()) viewModel.zrtpReadSas = toRead.uppercase(Locale.getDefault())
viewModel.zrtpListenSas = toListen.toUpperCase(Locale.getDefault()) viewModel.zrtpListenSas = toListen.uppercase(Locale.getDefault())
viewModel.showIcon = true viewModel.showIcon = true
viewModel.iconResource = R.drawable.security_2_indicator viewModel.iconResource = R.drawable.security_2_indicator

View file

@ -114,6 +114,7 @@ open class CallViewModel(val call: Call) : GenericContactViewModel(call.remoteAd
call.addListener(listener) call.addListener(listener)
isPaused.value = call.state == Call.State.Paused isPaused.value = call.state == Call.State.Paused
isOutgoingEarlyMedia.value = call.state == Call.State.OutgoingEarlyMedia
} }
override fun onCleared() { override fun onCleared() {
@ -138,7 +139,7 @@ open class CallViewModel(val call: Call) : GenericContactViewModel(call.remoteAd
} }
fun takeScreenshot() { fun takeScreenshot() {
if (call.currentParams.videoEnabled()) { if (call.currentParams.isVideoEnabled) {
val fileName = System.currentTimeMillis().toString() + ".jpeg" val fileName = System.currentTimeMillis().toString() + ".jpeg"
call.takeVideoSnapshot(FileUtils.getFileStoragePath(fileName).absolutePath) call.takeVideoSnapshot(FileUtils.getFileStoragePath(fileName).absolutePath)
} }

View file

@ -75,11 +75,11 @@ class CallsViewModel : ViewModel() {
} else if (call.state == Call.State.UpdatedByRemote) { } else if (call.state == Call.State.UpdatedByRemote) {
// If the correspondent asks to turn on video while audio call, // If the correspondent asks to turn on video while audio call,
// defer update until user has chosen whether to accept it or not // defer update until user has chosen whether to accept it or not
val remoteVideo = call.remoteParams?.videoEnabled() ?: false val remoteVideo = call.remoteParams?.isVideoEnabled ?: false
val localVideo = call.currentParams.videoEnabled() val localVideo = call.currentParams.isVideoEnabled
val autoAccept = call.core.videoActivationPolicy.automaticallyAccept val autoAccept = call.core.videoActivationPolicy.automaticallyAccept
if (remoteVideo && !localVideo && !autoAccept) { if (remoteVideo && !localVideo && !autoAccept) {
if (coreContext.core.videoCaptureEnabled() || coreContext.core.videoDisplayEnabled()) { if (coreContext.core.isVideoCaptureEnabled || coreContext.core.isVideoDisplayEnabled) {
call.deferUpdate() call.deferUpdate()
callUpdateEvent.value = Event(call) callUpdateEvent.value = Event(call)
} else { } else {

View file

@ -87,7 +87,11 @@ class ControlsViewModel : ViewModel() {
MutableLiveData<Event<Boolean>>() MutableLiveData<Event<Boolean>>()
} }
val askPermissionEvent: MutableLiveData<Event<String>> by lazy { val askAudioRecordPermissionEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
val askCameraPermissionEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>() MutableLiveData<Event<String>>()
} }
@ -176,7 +180,7 @@ class ControlsViewModel : ViewModel() {
} }
if (coreContext.isVideoCallOrConferenceActive() && !PermissionHelper.get().hasCameraPermission()) { if (coreContext.isVideoCallOrConferenceActive() && !PermissionHelper.get().hasCameraPermission()) {
askPermissionEvent.value = Event(Manifest.permission.CAMERA) askCameraPermissionEvent.value = Event(Manifest.permission.CAMERA)
} }
updateUI() updateUI()
@ -244,13 +248,13 @@ class ControlsViewModel : ViewModel() {
fun toggleMuteMicrophone() { fun toggleMuteMicrophone() {
if (!PermissionHelper.get().hasRecordAudioPermission()) { if (!PermissionHelper.get().hasRecordAudioPermission()) {
askPermissionEvent.value = Event(Manifest.permission.RECORD_AUDIO) askAudioRecordPermissionEvent.value = Event(Manifest.permission.RECORD_AUDIO)
return return
} }
somethingClickedEvent.value = Event(true) somethingClickedEvent.value = Event(true)
val micEnabled = coreContext.core.micEnabled() val micEnabled = coreContext.core.isMicEnabled
coreContext.core.enableMic(!micEnabled) coreContext.core.isMicEnabled = !micEnabled
updateMuteMicState() updateMuteMicState()
} }
@ -279,7 +283,7 @@ class ControlsViewModel : ViewModel() {
fun toggleVideo() { fun toggleVideo() {
if (!PermissionHelper.get().hasCameraPermission()) { if (!PermissionHelper.get().hasCameraPermission()) {
askPermissionEvent.value = Event(Manifest.permission.CAMERA) askCameraPermissionEvent.value = Event(Manifest.permission.CAMERA)
return return
} }
@ -300,7 +304,7 @@ class ControlsViewModel : ViewModel() {
isVideoUpdateInProgress.value = true isVideoUpdateInProgress.value = true
val params = core.createCallParams(currentCall) val params = core.createCallParams(currentCall)
params?.enableVideo(!currentCall.currentParams.videoEnabled()) params?.isVideoEnabled = !currentCall.currentParams.isVideoEnabled
currentCall.update(params) currentCall.update(params)
} }
} }
@ -338,23 +342,27 @@ class ControlsViewModel : ViewModel() {
val currentCall = core.currentCall val currentCall = core.currentCall
val conference = core.conference val conference = core.conference
if (currentCall != null) { when {
if (currentCall.isRecording) { currentCall != null -> {
currentCall.stopRecording() if (currentCall.isRecording) {
} else { currentCall.stopRecording()
currentCall.startRecording() } else {
currentCall.startRecording()
}
isRecording.value = currentCall.isRecording
} }
isRecording.value = currentCall.isRecording conference != null -> {
} else if (conference != null) { val path = LinphoneUtils.getRecordingFilePathForConference()
val path = LinphoneUtils.getRecordingFilePathForConference() if (conference.isRecording) {
if (conference.isRecording) { conference.stopRecording()
conference.stopRecording() } else {
} else { conference.startRecording(path)
conference.startRecording(path) }
isRecording.value = conference.isRecording
}
else -> {
isRecording.value = false
} }
isRecording.value = conference.isRecording
} else {
isRecording.value = false
} }
if (closeMenu) toggleOptionsMenu() if (closeMenu) toggleOptionsMenu()
@ -378,7 +386,7 @@ class ControlsViewModel : ViewModel() {
somethingClickedEvent.value = Event(true) somethingClickedEvent.value = Event(true)
val core = coreContext.core val core = coreContext.core
val currentCallVideoEnabled = core.currentCall?.currentParams?.videoEnabled() ?: false val currentCallVideoEnabled = core.currentCall?.currentParams?.isVideoEnabled ?: false
val params = core.createConferenceParams() val params = core.createConferenceParams()
params.isVideoEnabled = currentCallVideoEnabled params.isVideoEnabled = currentCallVideoEnabled
@ -411,7 +419,7 @@ class ControlsViewModel : ViewModel() {
} }
fun updateMuteMicState() { 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 isMuteMicrophoneEnabled.value = coreContext.core.currentCall != null || coreContext.core.conference?.isIn == true
} }
@ -457,7 +465,7 @@ class ControlsViewModel : ViewModel() {
private fun updateVideoAvailable() { private fun updateVideoAvailable() {
val core = coreContext.core val core = coreContext.core
val currentCall = core.currentCall val currentCall = core.currentCall
isVideoAvailable.value = (core.videoCaptureEnabled() || core.videoPreviewEnabled()) && isVideoAvailable.value = (core.isVideoCaptureEnabled || core.isVideoPreviewEnabled) &&
( (
(currentCall != null && !currentCall.mediaInProgress()) || (currentCall != null && !currentCall.mediaInProgress()) ||
core.conference?.isIn == true core.conference?.isIn == true

View file

@ -60,10 +60,10 @@ class IncomingCallViewModel(call: Call) : CallViewModel(call) {
coreContext.core.addListener(listener) coreContext.core.addListener(listener)
screenLocked.value = false 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 && earlyMediaVideoEnabled.value = corePreferences.acceptEarlyMedia &&
call.state == Call.State.IncomingEarlyMedia && call.state == Call.State.IncomingEarlyMedia &&
call.currentParams.videoEnabled() call.currentParams.isVideoEnabled
} }
override fun onCleared() { override fun onCleared() {

View file

@ -37,6 +37,8 @@ import org.linphone.activities.main.chat.adapters.ChatMessagesListAdapter
import org.linphone.activities.main.chat.viewmodels.* import org.linphone.activities.main.chat.viewmodels.*
import org.linphone.activities.main.viewmodels.ListTopBarViewModel import org.linphone.activities.main.viewmodels.ListTopBarViewModel
import org.linphone.core.ChatRoom import org.linphone.core.ChatRoom
import org.linphone.core.ChatRoomListenerStub
import org.linphone.core.EventLog
import org.linphone.core.Factory import org.linphone.core.Factory
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.databinding.ChatBubbleActivityBinding 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -86,11 +94,6 @@ class ChatBubbleActivity : GenericActivity() {
return 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( viewModel = ViewModelProvider(
this, this,
ChatRoomViewModelFactory(chatRoom) ChatRoomViewModelFactory(chatRoom)
@ -119,38 +122,40 @@ class ChatBubbleActivity : GenericActivity() {
adapter.disableContextMenu() adapter.disableContextMenu()
adapter.openContentEvent.observe( adapter.openContentEvent.observe(
this, this
{ ) {
it.consume { content -> it.consume { content ->
if (content.isFileEncrypted) { if (content.isFileEncrypted) {
Toast.makeText(this, R.string.chat_bubble_cant_open_enrypted_file, Toast.LENGTH_LONG).show() Toast.makeText(
} else { this,
FileUtils.openFileInThirdPartyApp(this, content.filePath.orEmpty(), true) R.string.chat_bubble_cant_open_enrypted_file,
} Toast.LENGTH_LONG
).show()
} else {
FileUtils.openFileInThirdPartyApp(this, content.filePath.orEmpty(), true)
} }
} }
) }
val layoutManager = LinearLayoutManager(this) val layoutManager = LinearLayoutManager(this)
layoutManager.stackFromEnd = true layoutManager.stackFromEnd = true
binding.chatMessagesList.layoutManager = layoutManager binding.chatMessagesList.layoutManager = layoutManager
listViewModel.events.observe( listViewModel.events.observe(
this, this
{ events -> ) { events ->
adapter.submitList(events) adapter.submitList(events)
} }
)
chatSendingViewModel.textToSend.observe( chatSendingViewModel.textToSend.observe(
this, this
{ ) {
chatSendingViewModel.onTextToSendChanged(it) chatSendingViewModel.onTextToSendChanged(it)
} }
)
binding.setOpenAppClickListener { binding.setOpenAppClickListener {
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
coreContext.notificationsManager.changeDismissNotificationUponReadForChatRoom(viewModel.chatRoom, false)
val intent = Intent(this, MainActivity::class.java) val intent = Intent(this, MainActivity::class.java)
intent.putExtra("RemoteSipUri", remoteSipUri) intent.putExtra("RemoteSipUri", remoteSipUri)
@ -173,6 +178,12 @@ class ChatBubbleActivity : GenericActivity() {
override fun onResume() { override fun onResume() {
super.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() val peerAddress = viewModel.chatRoom.peerAddress.asStringUriOnly()
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = peerAddress coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = peerAddress
coreContext.notificationsManager.resetChatNotificationCounterForSipUri(peerAddress) coreContext.notificationsManager.resetChatNotificationCounterForSipUri(peerAddress)
@ -185,7 +196,10 @@ class ChatBubbleActivity : GenericActivity() {
} }
override fun onPause() { override fun onPause() {
viewModel.chatRoom.removeListener(listener)
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
coreContext.notificationsManager.changeDismissNotificationUponReadForChatRoom(viewModel.chatRoom, false)
super.onPause() super.onPause()
} }

View file

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

View file

@ -30,8 +30,10 @@ import android.view.Gravity
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.doOnAttach
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@ -48,8 +50,7 @@ import kotlinx.coroutines.*
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
import org.linphone.activities.GenericActivity import org.linphone.activities.*
import org.linphone.activities.SnackBarActivity
import org.linphone.activities.assistant.AssistantActivity import org.linphone.activities.assistant.AssistantActivity
import org.linphone.activities.main.viewmodels.CallOverlayViewModel import org.linphone.activities.main.viewmodels.CallOverlayViewModel
import org.linphone.activities.main.viewmodels.SharedMainViewModel import org.linphone.activities.main.viewmodels.SharedMainViewModel
@ -112,6 +113,8 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val splashScreen = installSplashScreen()
binding = DataBindingUtil.setContentView(this, R.layout.main_activity) binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
binding.lifecycleOwner = this binding.lifecycleOwner = this
@ -122,30 +125,27 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
binding.callOverlayViewModel = callOverlayViewModel binding.callOverlayViewModel = callOverlayViewModel
sharedViewModel.toggleDrawerEvent.observe( sharedViewModel.toggleDrawerEvent.observe(
this, this
{ ) {
it.consume { it.consume {
if (binding.sideMenu.isDrawerOpen(Gravity.LEFT)) { if (binding.sideMenu.isDrawerOpen(Gravity.LEFT)) {
binding.sideMenu.closeDrawer(binding.sideMenuContent, true) binding.sideMenu.closeDrawer(binding.sideMenuContent, true)
} else { } else {
binding.sideMenu.openDrawer(binding.sideMenuContent, true) binding.sideMenu.openDrawer(binding.sideMenuContent, true)
}
} }
} }
) }
coreContext.callErrorMessageResourceId.observe( coreContext.callErrorMessageResourceId.observe(
this, this
{ ) {
it.consume { message -> it.consume { message ->
showSnackBar(message) showSnackBar(message)
}
} }
) }
if (coreContext.core.accountList.isEmpty()) { if (coreContext.core.accountList.isEmpty()) {
if (corePreferences.firstStart) { if (corePreferences.firstStart) {
corePreferences.firstStart = false
startActivity(Intent(this, AssistantActivity::class.java)) startActivity(Intent(this, AssistantActivity::class.java))
} }
} }
@ -153,7 +153,14 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
tabsFragment = findViewById(R.id.tabs_fragment) tabsFragment = findViewById(R.id.tabs_fragment)
statusFragment = findViewById(R.id.status_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?) { 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() 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) { override fun showSnackBar(message: String) {
Snackbar.make(findViewById(R.id.coordinator), message, Snackbar.LENGTH_LONG).show() Snackbar.make(findViewById(R.id.coordinator), message, Snackbar.LENGTH_LONG).show()
} }
@ -195,6 +212,8 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
updateTabsFragmentVisibility() updateTabsFragmentVisibility()
} }
initOverlay()
if (intent != null) handleIntentParams(intent) if (intent != null) handleIntentParams(intent)
} }
@ -209,7 +228,7 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
destination: NavDestination, destination: NavDestination,
arguments: Bundle? arguments: Bundle?
) { ) {
currentFocus?.hideKeyboard() hideKeyboard()
if (statusFragment.visibility == View.GONE) { if (statusFragment.visibility == View.GONE) {
statusFragment.visibility = View.VISIBLE statusFragment.visibility = View.VISIBLE
} }
@ -222,6 +241,10 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
updateTabsFragmentVisibility() updateTabsFragmentVisibility()
} }
fun hideKeyboard() {
currentFocus?.hideKeyboard()
}
private fun updateTabsFragmentVisibility() { private fun updateTabsFragmentVisibility() {
tabsFragment.visibility = if (tabsFragmentVisible1 && tabsFragmentVisible2) View.VISIBLE else View.GONE tabsFragment.visibility = if (tabsFragmentVisible1 && tabsFragmentVisible2) View.VISIBLE else View.GONE
} }
@ -253,9 +276,8 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
if (uri != null) { if (uri != null) {
val contactId = coreContext.contactsManager.getAndroidContactIdFromUri(uri) val contactId = coreContext.contactsManager.getAndroidContactIdFromUri(uri)
if (contactId != null) { if (contactId != null) {
val deepLink = "linphone-android://contact/view/$contactId" Log.i("[Main Activity] Found contact URI parameter in intent: $uri")
Log.i("[Main Activity] Found contact URI parameter in intent: $uri, starting deep link: $deepLink") navigateToContact(contactId)
findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
} }
} }
} else { } else {
@ -282,9 +304,8 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
when { when {
intent.hasExtra("ContactId") -> { intent.hasExtra("ContactId") -> {
val id = intent.getStringExtra("ContactId") val id = intent.getStringExtra("ContactId")
val deepLink = "linphone-android://contact/view/$id" Log.i("[Main Activity] Found contact ID in extras: $id")
Log.i("[Main Activity] Found contact id parameter in intent: $id, starting deep link: $deepLink") navigateToContact(id)
findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
} }
intent.hasExtra("Chat") -> { intent.hasExtra("Chat") -> {
if (corePreferences.disableChat) return if (corePreferences.disableChat) return
@ -293,10 +314,10 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
val peerAddress = intent.getStringExtra("RemoteSipUri") val peerAddress = intent.getStringExtra("RemoteSipUri")
val localAddress = intent.getStringExtra("LocalSipUri") val localAddress = intent.getStringExtra("LocalSipUri")
Log.i("[Main Activity] Found chat room intent extra: local SIP URI=[$localAddress], peer SIP URI=[$peerAddress]") 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 { } else {
Log.i("[Main Activity] Found chat intent extra, go to chat rooms list") 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") -> { intent.hasExtra("Dialer") -> {
@ -427,12 +448,11 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
addressToIM = addressToIM.substring("mmsto:".length) addressToIM = addressToIM.substring("mmsto:".length)
} }
val peerAddress = coreContext.core.interpretUrl(addressToIM)?.asStringUriOnly()
val localAddress = val localAddress =
coreContext.core.defaultAccount?.params?.identityAddress?.asStringUriOnly() coreContext.core.defaultAccount?.params?.identityAddress?.asStringUriOnly()
val deepLink = "linphone-android://chat-room/$localAddress/$peerAddress" val peerAddress = coreContext.core.interpretUrl(addressToIM)?.asStringUriOnly()
Log.i("[Main Activity] Starting deep link: $deepLink") Log.i("[Main Activity] Navigating to chat room with local [$localAddress] and peer [$peerAddress] addresses")
findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink)) navigateToChatRoom(localAddress, peerAddress)
} else { } else {
val shortcutId = intent.getStringExtra("android.intent.extra.shortcut.ID") // Intent.EXTRA_SHORTCUT_ID val shortcutId = intent.getStringExtra("android.intent.extra.shortcut.ID") // Intent.EXTRA_SHORTCUT_ID
if (shortcutId != null) { if (shortcutId != null) {
@ -440,7 +460,7 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
handleLocusOrShortcut(shortcutId) handleLocusOrShortcut(shortcutId)
} else { } else {
Log.i("[Main Activity] Going into chat rooms list") 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) { if (split.size == 2) {
val localAddress = split[0] val localAddress = split[0]
val peerAddress = split[1] val peerAddress = split[1]
val deepLink = "linphone-android://chat-room/$localAddress/$peerAddress" Log.i("[Main Activity] Navigating to chat room with local [$localAddress] and peer [$peerAddress] addresses, computed from shortcut/locus id")
findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink)) navigateToChatRoom(localAddress, peerAddress)
} else { } else {
Log.e("[Main Activity] Failed to parse shortcut/locus id: $id") Log.e("[Main Activity] Failed to parse shortcut/locus id: $id, going to chat rooms list")
findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_masterChatRoomsFragment) navigateToChatRooms()
} }
} }

View file

@ -58,5 +58,13 @@ class AboutFragment : SecureFragment<AboutFragmentBinding>() {
) )
startActivity(browserIntent) startActivity(browserIntent)
} }
binding.setWeblateClickListener {
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse(getString(R.string.about_weblate_link))
)
startActivity(browserIntent)
}
} }
} }

View file

@ -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. // True if we are still waiting for the last set of data to load.
private var loading = true 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. // 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, // We are given a few useful parameters to help us work out if we need to load some more data,

View file

@ -86,6 +86,10 @@ class ChatMessagesListAdapter(
MutableLiveData<Event<Content>>() MutableLiveData<Event<Content>>()
} }
val sipUriClickedEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
val scrollToChatMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy { val scrollToChatMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
MutableLiveData<Event<ChatMessage>>() MutableLiveData<Event<ChatMessage>>()
} }
@ -94,6 +98,10 @@ class ChatMessagesListAdapter(
override fun onContentClicked(content: Content) { override fun onContentClicked(content: Content) {
openContentEvent.value = Event(content) openContentEvent.value = Event(content)
} }
override fun onSipAddressClicked(sipUri: String) {
sipUriClickedEvent.value = Event(sipUri)
}
} }
private var contextMenuDisabled: Boolean = false private var contextMenuDisabled: Boolean = false
@ -211,15 +219,14 @@ class ChatMessagesListAdapter(
// This is for item selection through ListTopBarFragment // This is for item selection through ListTopBarFragment
selectionListViewModel = selectionViewModel selectionListViewModel = selectionViewModel
selectionViewModel.isEditionEnabled.observe( selectionViewModel.isEditionEnabled.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
position = adapterPosition position = bindingAdapterPosition
} }
)
setClickListener { setClickListener {
if (selectionViewModel.isEditionEnabled.value == true) { if (selectionViewModel.isEditionEnabled.value == true) {
selectionViewModel.onToggleSelect(adapterPosition) selectionViewModel.onToggleSelect(bindingAdapterPosition)
} }
} }
@ -234,8 +241,8 @@ class ChatMessagesListAdapter(
var hasPrevious = false var hasPrevious = false
var hasNext = false var hasNext = false
if (adapterPosition > 0) { if (bindingAdapterPosition > 0) {
val previousItem = getItem(adapterPosition - 1) val previousItem = getItem(bindingAdapterPosition - 1)
if (previousItem.eventLog.type == EventLog.Type.ConferenceChatMessage) { if (previousItem.eventLog.type == EventLog.Type.ConferenceChatMessage) {
val previousMessage = previousItem.eventLog.chatMessage val previousMessage = previousItem.eventLog.chatMessage
if (previousMessage != null && previousMessage.fromAddress.weakEqual(chatMessage.fromAddress)) { if (previousMessage != null && previousMessage.fromAddress.weakEqual(chatMessage.fromAddress)) {
@ -246,8 +253,8 @@ class ChatMessagesListAdapter(
} }
} }
if (adapterPosition >= 0 && adapterPosition < itemCount - 1) { if (bindingAdapterPosition >= 0 && bindingAdapterPosition < itemCount - 1) {
val nextItem = getItem(adapterPosition + 1) val nextItem = getItem(bindingAdapterPosition + 1)
if (nextItem.eventLog.type == EventLog.Type.ConferenceChatMessage) { if (nextItem.eventLog.type == EventLog.Type.ConferenceChatMessage) {
val nextMessage = nextItem.eventLog.chatMessage val nextMessage = nextItem.eventLog.chatMessage
if (nextMessage != null && nextMessage.fromAddress.weakEqual(chatMessage.fromAddress)) { 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() val itemSize = AppUtils.getDimension(R.dimen.chat_message_popup_item_height).toInt()
var totalSize = itemSize * 7 var totalSize = itemSize * 7
if (chatMessage.chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) || if (chatMessage.chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) {
chatMessage.state == ChatMessage.State.NotDelivered // No message id
) { // No message id
popupView.imdnHidden = true popupView.imdnHidden = true
totalSize -= itemSize totalSize -= itemSize
} }
@ -347,7 +353,7 @@ class ChatMessagesListAdapter(
private fun resendMessage() { private fun resendMessage() {
val chatMessage = binding.data?.chatMessage val chatMessage = binding.data?.chatMessage
if (chatMessage != null) { if (chatMessage != null) {
chatMessage.userData = adapterPosition chatMessage.userData = bindingAdapterPosition
resendMessageEvent.value = Event(chatMessage) resendMessageEvent.value = Event(chatMessage)
} }
} }
@ -389,7 +395,7 @@ class ChatMessagesListAdapter(
private fun deleteMessage() { private fun deleteMessage() {
val chatMessage = binding.data?.chatMessage val chatMessage = binding.data?.chatMessage
if (chatMessage != null) { if (chatMessage != null) {
chatMessage.userData = adapterPosition chatMessage.userData = bindingAdapterPosition
deleteMessageEvent.value = Event(chatMessage) deleteMessageEvent.value = Event(chatMessage)
} }
} }
@ -417,15 +423,14 @@ class ChatMessagesListAdapter(
// This is for item selection through ListTopBarFragment // This is for item selection through ListTopBarFragment
selectionListViewModel = selectionViewModel selectionListViewModel = selectionViewModel
selectionViewModel.isEditionEnabled.observe( selectionViewModel.isEditionEnabled.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
position = adapterPosition position = bindingAdapterPosition
} }
)
binding.setClickListener { binding.setClickListener {
if (selectionViewModel.isEditionEnabled.value == true) { if (selectionViewModel.isEditionEnabled.value == true) {
selectionViewModel.onToggleSelect(adapterPosition) selectionViewModel.onToggleSelect(bindingAdapterPosition)
} }
} }
@ -452,8 +457,18 @@ private class ChatMessageDiffCallback : DiffUtil.ItemCallback<EventLogData>() {
oldItem: EventLogData, oldItem: EventLogData,
newItem: EventLogData newItem: EventLogData
): Boolean { ): Boolean {
return if (newItem.eventLog.type == EventLog.Type.ConferenceChatMessage) { return if (oldItem.eventLog.type == EventLog.Type.ConferenceChatMessage &&
newItem.eventLog.chatMessage?.state == ChatMessage.State.Displayed newItem.eventLog.type == EventLog.Type.ConferenceChatMessage
} else true ) {
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
}
} }
} }

View file

@ -73,17 +73,16 @@ class ChatRoomsListAdapter(
// This is for item selection through ListTopBarFragment // This is for item selection through ListTopBarFragment
selectionListViewModel = selectionViewModel selectionListViewModel = selectionViewModel
selectionViewModel.isEditionEnabled.observe( selectionViewModel.isEditionEnabled.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
position = adapterPosition position = bindingAdapterPosition
} }
)
forwardPending = isForwardPending forwardPending = isForwardPending
setClickListener { setClickListener {
if (selectionViewModel.isEditionEnabled.value == true) { if (selectionViewModel.isEditionEnabled.value == true) {
selectionViewModel.onToggleSelect(adapterPosition) selectionViewModel.onToggleSelect(bindingAdapterPosition)
} else { } else {
selectedChatRoomEvent.value = Event(chatRoomViewModel.chatRoom) selectedChatRoomEvent.value = Event(chatRoomViewModel.chatRoom)
} }
@ -116,6 +115,6 @@ private class ChatRoomDiffCallback : DiffUtil.ItemCallback<ChatRoomViewModel>()
oldItem: ChatRoomViewModel, oldItem: ChatRoomViewModel,
newItem: ChatRoomViewModel newItem: ChatRoomViewModel
): Boolean { ): Boolean {
return newItem.unreadMessagesCount.value == 0 return false // To force redraw when contacts are updated
} }
} }

View file

@ -44,7 +44,6 @@ import org.linphone.utils.ImageUtils
class ChatMessageContentData( class ChatMessageContentData(
private val chatMessage: ChatMessage, private val chatMessage: ChatMessage,
private val contentIndex: Int, private val contentIndex: Int,
) { ) {
var listener: OnContentClickedListener? = null var listener: OnContentClickedListener? = null
@ -60,7 +59,6 @@ class ChatMessageContentData(
val fileName = MutableLiveData<String>() val fileName = MutableLiveData<String>()
val filePath = MutableLiveData<String>() val filePath = MutableLiveData<String>()
val fileSize = MutableLiveData<String>()
val downloadable = MutableLiveData<Boolean>() val downloadable = MutableLiveData<Boolean>()
val downloadEnabled = MutableLiveData<Boolean>() val downloadEnabled = MutableLiveData<Boolean>()
@ -72,13 +70,11 @@ class ChatMessageContentData(
val formattedDuration = MutableLiveData<String>() val formattedDuration = MutableLiveData<String>()
val voiceRecordPlayingPosition = MutableLiveData<Int>() val voiceRecordPlayingPosition = MutableLiveData<Int>()
val isVoiceRecordPlaying = MutableLiveData<Boolean>() val isVoiceRecordPlaying = MutableLiveData<Boolean>()
var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
val isAlone: Boolean val isAlone: Boolean
get() { get() {
var count = 0 var count = 0
for (content in chatMessage.contents) { for (content in chatMessage.contents) {
val content = getContent()
if (content.isFileTransfer || content.isFile) { if (content.isFileTransfer || content.isFile) {
count += 1 count += 1
} }
@ -86,7 +82,9 @@ class ChatMessageContentData(
return count == 1 return count == 1
} }
var isFileEncrypted: Boolean = false private var isFileEncrypted: Boolean = false
private var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
private lateinit var voiceRecordingPlayer: Player private lateinit var voiceRecordingPlayer: Player
private val playerListener = PlayerListener { private val playerListener = PlayerListener {
@ -145,13 +143,7 @@ class ChatMessageContentData(
fun destroy() { fun destroy() {
scope.cancel() scope.cancel()
val path = filePath.value.orEmpty() deletePlainFilePath()
if (path.isNotEmpty() && isFileEncrypted) {
Log.i("[Content] Deleting file used for preview: $path")
FileUtils.deleteFile(path)
filePath.value = ""
}
chatMessage.removeListener(chatMessageListener) chatMessage.removeListener(chatMessageListener)
if (this::voiceRecordingPlayer.isInitialized) { if (this::voiceRecordingPlayer.isInitialized) {
@ -181,9 +173,22 @@ class ChatMessageContentData(
listener?.onContentClicked(getContent()) 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() { private fun updateContent() {
Log.i("[Content] Updating content")
deletePlainFilePath()
val content = getContent() val content = getContent()
isFileEncrypted = content.isFileEncrypted isFileEncrypted = content.isFileEncrypted
Log.i("[Content] Is ${if (content.isFile) "file" else "file transfer"} content encrypted ? $isFileEncrypted")
filePath.value = "" filePath.value = ""
fileName.value = if (content.name.isNullOrEmpty() && !content.filePath.isNullOrEmpty()) { fileName.value = if (content.name.isNullOrEmpty() && !content.filePath.isNullOrEmpty()) {
@ -193,14 +198,18 @@ class ChatMessageContentData(
} }
// Display download size and underline text // Display download size and underline text
fileSize.value = AppUtils.bytesToDisplayableSize(content.fileSize.toLong()) val fileSize = AppUtils.bytesToDisplayableSize(content.fileSize.toLong())
val spannable = SpannableString("${AppUtils.getString(R.string.chat_message_download_file)} (${fileSize.value})") val spannable = SpannableString("${AppUtils.getString(R.string.chat_message_download_file)} ($fileSize)")
spannable.setSpan(UnderlineSpan(), 0, spannable.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) spannable.setSpan(UnderlineSpan(), 0, spannable.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
downloadLabel.value = spannable downloadLabel.value = spannable
if (content.isFile || (content.isFileTransfer && chatMessage.isOutgoing)) { if (content.isFile || (content.isFileTransfer && chatMessage.isOutgoing)) {
Log.i("[Content] Is content encrypted ? $isFileEncrypted") val path = if (isFileEncrypted) {
val path = if (isFileEncrypted) content.plainFilePath else content.filePath ?: "" Log.i("[Content] Content is encrypted, requesting plain file path")
content.plainFilePath
} else {
content.filePath ?: ""
}
downloadable.value = content.filePath.orEmpty().isEmpty() downloadable.value = content.filePath.orEmpty().isEmpty()
if (path.isNotEmpty()) { if (path.isNotEmpty()) {
@ -226,7 +235,7 @@ class ChatMessageContentData(
} }
} }
} else { } 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 isImage.value = false
isVideo.value = false isVideo.value = false
isAudio.value = false isAudio.value = false
@ -297,8 +306,9 @@ class ChatMessageContentData(
private fun initVoiceRecordPlayer() { private fun initVoiceRecordPlayer() {
Log.i("[Voice Recording] Creating player for voice record") 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 // If none are available, default one will be used
var headphonesCard: String? = null
var speakerCard: String? = null var speakerCard: String? = null
var earpieceCard: String? = null var earpieceCard: String? = null
for (device in coreContext.core.audioDevices) { for (device in coreContext.core.audioDevices) {
@ -307,12 +317,14 @@ class ChatMessageContentData(
speakerCard = device.id speakerCard = device.id
} else if (device.type == AudioDevice.Type.Earpiece) { } else if (device.type == AudioDevice.Type.Earpiece) {
earpieceCard = device.id 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) { if (localPlayer != null) {
voiceRecordingPlayer = localPlayer voiceRecordingPlayer = localPlayer
} else { } else {
@ -321,8 +333,7 @@ class ChatMessageContentData(
} }
voiceRecordingPlayer.addListener(playerListener) voiceRecordingPlayer.addListener(playerListener)
val content = getContent() val path = filePath.value
val path = if (content.isFileEncrypted) content.plainFilePath else content.filePath ?: ""
voiceRecordingPlayer.open(path.orEmpty()) voiceRecordingPlayer.open(path.orEmpty())
voiceRecordDuration.value = voiceRecordingPlayer.duration voiceRecordDuration.value = voiceRecordingPlayer.duration
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(voiceRecordingPlayer.duration) // is already in milliseconds formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(voiceRecordingPlayer.duration) // is already in milliseconds
@ -346,4 +357,6 @@ class ChatMessageContentData(
interface OnContentClickedListener { interface OnContentClickedListener {
fun onContentClicked(content: Content) fun onContentClicked(content: Content)
fun onSipAddressClicked(sipUri: String)
} }

View file

@ -24,12 +24,14 @@ import android.text.Spannable
import android.text.util.Linkify import android.text.util.Linkify
import androidx.core.text.util.LinkifyCompat import androidx.core.text.util.LinkifyCompat
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import java.util.regex.Pattern
import org.linphone.R import org.linphone.R
import org.linphone.contact.GenericContactData import org.linphone.contact.GenericContactData
import org.linphone.core.ChatMessage import org.linphone.core.ChatMessage
import org.linphone.core.ChatMessageListenerStub import org.linphone.core.ChatMessageListenerStub
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils import org.linphone.utils.AppUtils
import org.linphone.utils.PatternClickableSpan
import org.linphone.utils.TimestampUtils import org.linphone.utils.TimestampUtils
class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMessage.fromAddress) { class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMessage.fromAddress) {
@ -59,6 +61,9 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
val replyData = MutableLiveData<ChatMessageData>() val replyData = MutableLiveData<ChatMessageData>()
var hasPreviousMessage = false
var hasNextMessage = false
private var countDownTimer: CountDownTimer? = null private var countDownTimer: CountDownTimer? = null
private val listener = object : ChatMessageListenerStub() { private val listener = object : ChatMessageListenerStub() {
@ -106,6 +111,11 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
} }
fun updateBubbleBackground(hasPrevious: Boolean, hasNext: Boolean) { fun updateBubbleBackground(hasPrevious: Boolean, hasNext: Boolean) {
hasPreviousMessage = hasPrevious
hasNextMessage = hasNext
hideTime.value = false
hideAvatar.value = false
if (hasPrevious) { if (hasPrevious) {
hideTime.value = true hideTime.value = true
} }
@ -165,16 +175,25 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
val list = arrayListOf<ChatMessageContentData>() val list = arrayListOf<ChatMessageContentData>()
val contentsList = chatMessage.contents val contentsList = chatMessage.contents
for (index in 0 until contentsList.size) { for (index in contentsList.indices) {
val content = contentsList[index] val content = contentsList[index]
if (content.isFileTransfer || content.isFile) { if (content.isFileTransfer || content.isFile) {
val data = ChatMessageContentData(chatMessage, index) val data = ChatMessageContentData(chatMessage, index)
data.listener = contentListener data.listener = contentListener
list.add(data) list.add(data)
} else if (content.isText) { } 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) 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)
} }
} }

View file

@ -93,62 +93,55 @@ class ChatRoomCreationFragment : SecureFragment<ChatRoomCreationFragmentBinding>
} }
viewModel.contactsList.observe( viewModel.contactsList.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
adapter.submitList(it) adapter.submitList(it)
} }
)
viewModel.isEncrypted.observe( viewModel.isEncrypted.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
adapter.updateSecurity(it) adapter.updateSecurity(it)
} }
)
viewModel.sipContactsSelected.observe( viewModel.sipContactsSelected.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
viewModel.updateContactsList() viewModel.updateContactsList()
} }
)
viewModel.selectedAddresses.observe( viewModel.selectedAddresses.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
adapter.updateSelectedAddresses(it) adapter.updateSelectedAddresses(it)
} }
)
viewModel.chatRoomCreatedEvent.observe( viewModel.chatRoomCreatedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { chatRoom -> it.consume { chatRoom ->
sharedViewModel.selectedChatRoom.value = chatRoom sharedViewModel.selectedChatRoom.value = chatRoom
navigateToChatRoom(AppUtils.createBundleWithSharedTextAndFiles(sharedViewModel)) navigateToChatRoom(AppUtils.createBundleWithSharedTextAndFiles(sharedViewModel))
}
} }
) }
viewModel.filter.observe( viewModel.filter.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
viewModel.applyFilter() viewModel.applyFilter()
} }
)
adapter.selectedContact.observe( adapter.selectedContact.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { searchResult -> it.consume { searchResult ->
if (createGroup) { if (createGroup) {
viewModel.toggleSelectionForSearchResult(searchResult) viewModel.toggleSelectionForSearchResult(searchResult)
} else { } else {
viewModel.createOneToOneChat(searchResult) viewModel.createOneToOneChat(searchResult)
}
} }
} }
) }
addParticipantsFromSharedViewModel() addParticipantsFromSharedViewModel()
@ -160,13 +153,12 @@ class ChatRoomCreationFragment : SecureFragment<ChatRoomCreationFragmentBinding>
} }
viewModel.onErrorEvent.observe( viewModel.onErrorEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { messageResourceId -> it.consume { messageResourceId ->
(activity as MainActivity).showSnackBar(messageResourceId) (activity as MainActivity).showSnackBar(messageResourceId)
}
} }
) }
if (!PermissionHelper.get().hasReadContactsPermission()) { if (!PermissionHelper.get().hasReadContactsPermission()) {
Log.i("[Chat Room Creation] Asking for READ_CONTACTS permission") Log.i("[Chat Room Creation] Asking for READ_CONTACTS permission")

View file

@ -29,19 +29,20 @@ import android.provider.MediaStore
import android.view.* import android.view.*
import android.view.ViewTreeObserver.OnGlobalLayoutListener import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.PopupWindow import android.widget.PopupWindow
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.view.doOnPreDraw import androidx.core.view.doOnPreDraw
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import java.io.File import java.io.File
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
@ -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.ChatMessageData
import org.linphone.activities.main.chat.data.EventLogData import org.linphone.activities.main.chat.data.EventLogData
import org.linphone.activities.main.chat.viewmodels.* 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.fragments.MasterFragment
import org.linphone.activities.main.viewmodels.DialogViewModel import org.linphone.activities.main.viewmodels.DialogViewModel
import org.linphone.activities.main.viewmodels.SharedMainViewModel import org.linphone.activities.main.viewmodels.SharedMainViewModel
@ -107,6 +109,20 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
super.onDestroyView() super.onDestroyView()
} }
override fun onSaveInstanceState(outState: Bundle) {
if (::sharedViewModel.isInitialized) {
val chatRoom = sharedViewModel.selectedChatRoom.value
if (chatRoom != null) {
outState.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly())
outState.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly())
Log.i("[Chat Room] Saving current chat room local & remote addresses in save instance state")
}
} else {
Log.w("[Chat Room] Can't save instance state, sharedViewModel hasn't been initialized yet")
}
super.onSaveInstanceState(outState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -119,20 +135,18 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false
view.doOnPreDraw { val localSipUri = arguments?.getString("LocalSipUri") ?: savedInstanceState?.getString("LocalSipUri")
// Notifies fragment is ready to be drawn val remoteSipUri = arguments?.getString("RemoteSipUri") ?: savedInstanceState?.getString("RemoteSipUri")
sharedViewModel.chatRoomFragmentOpenedEvent.value = Event(true)
}
val localSipUri = arguments?.getString("LocalSipUri")
val remoteSipUri = arguments?.getString("RemoteSipUri")
val textToShare = arguments?.getString("TextToShare") val textToShare = arguments?.getString("TextToShare")
val filesToShare = arguments?.getStringArrayList("FilesToShare") val filesToShare = arguments?.getStringArrayList("FilesToShare")
if (remoteSipUri != null && arguments?.getString("RemoteSipUri") == null) {
Log.w("[Chat Room] Chat room will be restored from saved instance state")
}
arguments?.clear() arguments?.clear()
if (localSipUri != null && remoteSipUri != null) { if (localSipUri != null && remoteSipUri != null) {
Log.i("[Chat Room] Found local [$localSipUri] & remote [$remoteSipUri] addresses in arguments") Log.i("[Chat Room] Found local [$localSipUri] & remote [$remoteSipUri] addresses in arguments or saved instance state")
val localAddress = Factory.instance().createAddress(localSipUri) val localAddress = Factory.instance().createAddress(localSipUri)
val remoteSipAddress = Factory.instance().createAddress(remoteSipUri) val remoteSipAddress = Factory.instance().createAddress(remoteSipUri)
@ -152,14 +166,29 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
return return
} }
view.doOnPreDraw {
// Notifies fragment is ready to be drawn
sharedViewModel.chatRoomFragmentOpenedEvent.value = Event(true)
}
Compatibility.setLocusIdInContentCaptureSession(binding.root, chatRoom) Compatibility.setLocusIdInContentCaptureSession(binding.root, chatRoom)
isSecure = chatRoom.currentParams.encryptionEnabled() isSecure = chatRoom.currentParams.isEncryptionEnabled
viewModel = ViewModelProvider( val chatRoomsListViewModel: ChatRoomsListViewModel = requireActivity().run {
ViewModelProvider(this)[ChatRoomsListViewModel::class.java]
}
val chatRoomViewModel = chatRoomsListViewModel.chatRooms.value.orEmpty().find {
it.chatRoom == chatRoom
}
if (chatRoomViewModel == null) {
Log.w("[Chat Room] Couldn't find existing view model, will create a new one!")
}
viewModel = chatRoomViewModel ?: ViewModelProvider(
this, this,
ChatRoomViewModelFactory(chatRoom) ChatRoomViewModelFactory(chatRoom)
)[ChatRoomViewModel::class.java] )[ChatRoomViewModel::class.java]
binding.viewModel = viewModel binding.viewModel = viewModel
chatSendingViewModel = ViewModelProvider( chatSendingViewModel = ViewModelProvider(
@ -193,24 +222,38 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
.addOnGlobalLayoutListener( .addOnGlobalLayoutListener(
object : OnGlobalLayoutListener { object : OnGlobalLayoutListener {
override fun onGlobalLayout() { override fun onGlobalLayout() {
binding.chatMessagesList if (isBindingAvailable()) {
.viewTreeObserver binding.chatMessagesList
.removeOnGlobalLayoutListener(this) .viewTreeObserver
Log.i("[Chat Room] Messages have been displayed, scrolling to first unread message if any") .removeOnGlobalLayoutListener(this)
scrollToFirstUnreadMessageOrBottom(false) Log.i("[Chat Room] Messages have been displayed, scrolling to first unread message if any")
scrollToFirstUnreadMessageOrBottom(false)
} else {
Log.e("[Chat Room] Binding not available in onGlobalLayout callback!")
}
} }
} }
) )
// Swipe action // Swipe action
/*val swipeConfiguration = RecyclerViewSwipeConfiguration() val swipeConfiguration = RecyclerViewSwipeConfiguration()
swipeConfiguration.leftToRightAction = RecyclerViewSwipeConfiguration.Action(icon = R.drawable.menu_reply_default) // Reply action can only be done on a ChatMessageEventLog
swipeConfiguration.leftToRightAction = RecyclerViewSwipeConfiguration.Action(
text = requireContext().getString(R.string.chat_message_context_menu_reply),
backgroundColor = ContextCompat.getColor(requireContext(), R.color.light_grey_color),
preventFor = ChatMessagesListAdapter.EventViewHolder::class.java
)
// Delete action can be done on any EventLog
swipeConfiguration.rightToLeftAction = RecyclerViewSwipeConfiguration.Action(
text = requireContext().getString(R.string.chat_message_context_menu_delete),
backgroundColor = ContextCompat.getColor(requireContext(), R.color.red_color)
)
val swipeListener = object : RecyclerViewSwipeListener { val swipeListener = object : RecyclerViewSwipeListener {
override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) { override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) {
adapter.notifyItemChanged(viewHolder.adapterPosition) adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
val chatMessageEventLog = adapter.currentList[viewHolder.adapterPosition] val chatMessageEventLog = adapter.currentList[viewHolder.bindingAdapterPosition]
val chatMessage = chatMessageEventLog.chatMessage val chatMessage = chatMessageEventLog.eventLog.chatMessage
if (chatMessage != null) { if (chatMessage != null) {
chatSendingViewModel.pendingChatMessageToReplyTo.value?.destroy() chatSendingViewModel.pendingChatMessageToReplyTo.value?.destroy()
chatSendingViewModel.pendingChatMessageToReplyTo.value = chatSendingViewModel.pendingChatMessageToReplyTo.value =
@ -219,10 +262,16 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
} }
} }
override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) {} override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) {
val position = viewHolder.bindingAdapterPosition
// adapter.notifyItemRemoved(viewHolder.bindingAdapterPosition)
val eventLog = adapter.currentList[position]
addDeleteMessageTaskToQueue(eventLog, position)
}
} }
RecyclerViewSwipeUtils(ItemTouchHelper.RIGHT, swipeConfiguration, swipeListener) RecyclerViewSwipeUtils(ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT, swipeConfiguration, swipeListener)
.attachToRecyclerView(binding.chatMessagesList)*/ .attachToRecyclerView(binding.chatMessagesList)
val chatScrollListener = object : ChatScrollListener(layoutManager) { val chatScrollListener = object : ChatScrollListener(layoutManager) {
override fun onLoadMore(totalItemsCount: Int) { override fun onLoadMore(totalItemsCount: Int) {
@ -236,7 +285,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
override fun onScrolledToEnd() { override fun onScrolledToEnd() {
viewModel.isUserScrollingUp.value = false viewModel.isUserScrollingUp.value = false
if (viewModel.unreadMessagesCount.value != 0) { if (viewModel.unreadMessagesCount.value != 0 && coreContext.notificationsManager.currentlyDisplayedChatRoomAddress != null) {
Log.i("[Chat Room] User has scrolled to the latest message, mark chat room as read") Log.i("[Chat Room] User has scrolled to the latest message, mark chat room as read")
viewModel.chatRoom.markAsRead() viewModel.chatRoom.markAsRead()
} }
@ -245,192 +294,204 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
binding.chatMessagesList.addOnScrollListener(chatScrollListener) binding.chatMessagesList.addOnScrollListener(chatScrollListener)
chatSendingViewModel.textToSend.observe( chatSendingViewModel.textToSend.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
chatSendingViewModel.onTextToSendChanged(it) chatSendingViewModel.onTextToSendChanged(it)
} }
)
chatSendingViewModel.requestRecordAudioPermissionEvent.observe( chatSendingViewModel.requestRecordAudioPermissionEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
Log.i("[Chat Room] Asking for RECORD_AUDIO permission") Log.i("[Chat Room] Asking for RECORD_AUDIO permission")
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 2) requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 2)
}
} }
) }
listViewModel.events.observe( listViewModel.events.observe(
viewLifecycleOwner, viewLifecycleOwner
{ events -> ) { events ->
adapter.setUnreadMessageCount(viewModel.chatRoom.unreadMessagesCount, viewModel.isUserScrollingUp.value == true) adapter.setUnreadMessageCount(
adapter.submitList(events) viewModel.chatRoom.unreadMessagesCount,
} viewModel.isUserScrollingUp.value == true
) )
adapter.submitList(events)
}
listViewModel.messageUpdatedEvent.observe( listViewModel.messageUpdatedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { position -> it.consume { position ->
adapter.notifyItemChanged(position) adapter.notifyItemChanged(position)
}
} }
) }
listViewModel.requestWriteExternalStoragePermissionEvent.observe( listViewModel.requestWriteExternalStoragePermissionEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), 1) requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
}
} }
) }
adapter.deleteMessageEvent.observe( adapter.deleteMessageEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { chatMessage -> it.consume { chatMessage ->
listViewModel.deleteMessage(chatMessage) listViewModel.deleteMessage(chatMessage)
} viewModel.updateLastMessageToDisplay()
} }
) }
adapter.resendMessageEvent.observe( adapter.resendMessageEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { chatMessage -> it.consume { chatMessage ->
listViewModel.resendMessage(chatMessage) listViewModel.resendMessage(chatMessage)
}
} }
) }
adapter.forwardMessageEvent.observe( adapter.forwardMessageEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { chatMessage -> it.consume { chatMessage ->
// Remove observer before setting the message to forward // Remove observer before setting the message to forward
// as we don't want to forward it in this chat room // as we don't want to forward it in this chat room
sharedViewModel.messageToForwardEvent.removeObservers(viewLifecycleOwner) sharedViewModel.messageToForwardEvent.removeObservers(viewLifecycleOwner)
sharedViewModel.messageToForwardEvent.value = Event(chatMessage) sharedViewModel.messageToForwardEvent.value = Event(chatMessage)
sharedViewModel.isPendingMessageForward.value = true sharedViewModel.isPendingMessageForward.value = true
if (sharedViewModel.isSlidingPaneSlideable.value == true) { if (sharedViewModel.isSlidingPaneSlideable.value == true) {
Log.i("[Chat Room] Forwarding message, going to chat rooms list") Log.i("[Chat Room] Forwarding message, going to chat rooms list")
sharedViewModel.closeSlidingPaneEvent.value = Event(true) sharedViewModel.closeSlidingPaneEvent.value = Event(true)
} else { } else {
navigateToEmptyChatRoom() navigateToEmptyChatRoom()
}
} }
} }
) }
adapter.replyMessageEvent.observe( adapter.replyMessageEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { chatMessage -> it.consume { chatMessage ->
chatSendingViewModel.pendingChatMessageToReplyTo.value?.destroy() chatSendingViewModel.pendingChatMessageToReplyTo.value?.destroy()
chatSendingViewModel.pendingChatMessageToReplyTo.value = ChatMessageData(chatMessage) chatSendingViewModel.pendingChatMessageToReplyTo.value =
chatSendingViewModel.isPendingAnswer.value = true ChatMessageData(chatMessage)
} chatSendingViewModel.isPendingAnswer.value = true
} }
) }
adapter.showImdnForMessageEvent.observe( adapter.showImdnForMessageEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { chatMessage -> it.consume { chatMessage ->
val args = Bundle() val args = Bundle()
args.putString("MessageId", chatMessage.messageId) args.putString("MessageId", chatMessage.messageId)
navigateToImdn(args) navigateToImdn(args)
}
} }
) }
adapter.addSipUriToContactEvent.observe( adapter.addSipUriToContactEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { sipUri -> it.consume { sipUri ->
Log.i("[Chat Room] Going to contacts list with SIP URI to add: $sipUri") Log.i("[Chat Room] Going to contacts list with SIP URI to add: $sipUri")
sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event(R.id.masterChatRoomsFragment) sharedViewModel.updateContactsAnimationsBasedOnDestination.value =
navigateToContacts(sipUri) Event(R.id.masterChatRoomsFragment)
} navigateToContacts(sipUri)
} }
) }
adapter.openContentEvent.observe( adapter.openContentEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { content -> it.consume { content ->
val path = content.filePath.orEmpty() val path = content.filePath.orEmpty()
if (!File(path).exists()) { if (!File(path).exists()) {
(requireActivity() as MainActivity).showSnackBar(R.string.chat_room_file_not_found) (requireActivity() as MainActivity).showSnackBar(R.string.chat_room_file_not_found)
} else { } else {
Log.i("[Chat Message] Opening file: $path") Log.i("[Chat Message] Opening file: $path")
sharedViewModel.contentToOpen.value = content sharedViewModel.contentToOpen.value = content
if (corePreferences.useInAppFileViewerForNonEncryptedFiles || content.isFileEncrypted) { if (corePreferences.useInAppFileViewerForNonEncryptedFiles || content.isFileEncrypted) {
val preventScreenshots = val preventScreenshots =
viewModel.chatRoom.currentParams.encryptionEnabled() viewModel.chatRoom.currentParams.isEncryptionEnabled
when { when {
FileUtils.isExtensionImage(path) -> navigateToImageFileViewer( FileUtils.isExtensionImage(path) -> navigateToImageFileViewer(
preventScreenshots preventScreenshots
) )
FileUtils.isExtensionVideo(path) -> navigateToVideoFileViewer( FileUtils.isExtensionVideo(path) -> navigateToVideoFileViewer(
preventScreenshots preventScreenshots
) )
FileUtils.isExtensionAudio(path) -> navigateToAudioFileViewer( FileUtils.isExtensionAudio(path) -> navigateToAudioFileViewer(
preventScreenshots preventScreenshots
) )
FileUtils.isExtensionPdf(path) -> navigateToPdfFileViewer( FileUtils.isExtensionPdf(path) -> navigateToPdfFileViewer(
preventScreenshots preventScreenshots
) )
FileUtils.isPlainTextFile(path) -> navigateToTextFileViewer( FileUtils.isPlainTextFile(path) -> navigateToTextFileViewer(
preventScreenshots preventScreenshots
) )
else -> { else -> {
if (content.isFileEncrypted) { if (content.isFileEncrypted) {
Log.w("[Chat Message] File is encrypted and can't be opened in one of our viewers...") Log.w("[Chat Message] File is encrypted and can't be opened in one of our viewers...")
showDialogForUserConsentBeforeExportingFileInThirdPartyApp(content) showDialogForUserConsentBeforeExportingFileInThirdPartyApp(
} else if (!FileUtils.openFileInThirdPartyApp(requireActivity(), path)) { content
showDialogToSuggestOpeningFileAsText() )
} } else if (!FileUtils.openFileInThirdPartyApp(
requireActivity(),
path
)
) {
showDialogToSuggestOpeningFileAsText()
} }
} }
} else { }
if (!FileUtils.openFileInThirdPartyApp(requireActivity(), path)) { } else {
showDialogToSuggestOpeningFileAsText() 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( adapter.scrollToChatMessageEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { chatMessage -> it.consume { chatMessage ->
val events = listViewModel.events.value.orEmpty() val events = listViewModel.events.value.orEmpty()
val eventLog = events.find { eventLog -> val eventLog = events.find { eventLog ->
if (eventLog.eventLog.type == EventLog.Type.ConferenceChatMessage) { if (eventLog.eventLog.type == EventLog.Type.ConferenceChatMessage) {
(eventLog.data as ChatMessageData).chatMessage.messageId == chatMessage.messageId (eventLog.data as ChatMessageData).chatMessage.messageId == chatMessage.messageId
} else false } else false
} }
val index = events.indexOf(eventLog) val index = events.indexOf(eventLog)
try { try {
if (corePreferences.enableAnimations) { if (corePreferences.enableAnimations) {
binding.chatMessagesList.smoothScrollToPosition(index) binding.chatMessagesList.smoothScrollToPosition(index)
} else { } else {
binding.chatMessagesList.scrollToPosition(index) binding.chatMessagesList.scrollToPosition(index)
}
} catch (iae: IllegalArgumentException) {
Log.e("[Chat Room] Can't scroll to position $index")
} }
} catch (iae: IllegalArgumentException) {
Log.e("[Chat Room] Can't scroll to position $index")
} }
} }
) }
binding.setBackClickListener { binding.setBackClickListener {
goBack() goBack()
@ -493,8 +554,20 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
false false
} }
binding.message.setControlEnterListener(object : RichEditTextSendListener {
override fun onControlEnterPressedAndReleased() {
Log.i("[Chat Room] Detected left control + enter key presses, sending message")
chatSendingViewModel.sendMessage()
}
})
binding.setCancelReplyToClickListener {
chatSendingViewModel.cancelReply()
}
binding.setScrollToBottomClickListener { binding.setScrollToBottomClickListener {
scrollToFirstUnreadMessageOrBottom(true) scrollToFirstUnreadMessageOrBottom(true)
viewModel.isUserScrollingUp.value = false
} }
if (textToShare?.isNotEmpty() == true) { if (textToShare?.isNotEmpty() == true) {
@ -509,33 +582,43 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
} }
sharedViewModel.richContentUri.observe( sharedViewModel.richContentUri.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { uri -> it.consume { uri ->
Log.i("[Chat] Found rich content URI: $uri") Log.i("[Chat] Found rich content URI: $uri")
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
val path = FileUtils.getFilePath(requireContext(), uri) val path = FileUtils.getFilePath(requireContext(), uri)
Log.i("[Chat] Rich content URI: $uri matching path is: $path") Log.i("[Chat] Rich content URI: $uri matching path is: $path")
if (path != null) { if (path != null) {
chatSendingViewModel.addAttachment(path) chatSendingViewModel.addAttachment(path)
}
} }
} }
} }
} }
) }
sharedViewModel.messageToForwardEvent.observe( sharedViewModel.messageToForwardEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { chatMessage -> it.consume { chatMessage ->
Log.i("[Chat Room] Found message to transfer") Log.i("[Chat Room] Found message to transfer")
showForwardConfirmationDialog(chatMessage) showForwardConfirmationDialog(chatMessage)
sharedViewModel.isPendingMessageForward.value = false sharedViewModel.isPendingMessageForward.value = false
}
} }
) }
binding.stubbedMessageToReplyTo.setOnInflateListener { _, inflated ->
Log.i("[Chat Room] Replying to message layout inflated")
val binding = DataBindingUtil.bind<ViewDataBinding>(inflated)
binding?.lifecycleOwner = viewLifecycleOwner
}
binding.stubbedVoiceRecording.setOnInflateListener { _, inflated ->
Log.i("[Chat Room] Voice recording layout inflated")
val binding = DataBindingUtil.bind<ViewDataBinding>(inflated)
binding?.lifecycleOwner = viewLifecycleOwner
}
} }
override fun deleteItems(indexesOfItemToDelete: ArrayList<Int>) { override fun deleteItems(indexesOfItemToDelete: ArrayList<Int>) {
@ -545,6 +628,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
list.add(eventLog) list.add(eventLog)
} }
listViewModel.deleteEventLogs(list) listViewModel.deleteEventLogs(list)
viewModel.updateLastMessageToDisplay()
} }
override fun onRequestPermissionsResult( override fun onRequestPermissionsResult(
@ -607,6 +691,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
} }
override fun goBack() { override fun goBack() {
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
if (!findNavController().popBackStack()) { if (!findNavController().popBackStack()) {
if (sharedViewModel.isSlidingPaneSlideable.value == true) { if (sharedViewModel.isSlidingPaneSlideable.value == true) {
if (_adapter != null) { if (_adapter != null) {
@ -772,6 +857,35 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
popupWindow.showAsDropDown(binding.menu, 0, 0, Gravity.BOTTOM) popupWindow.showAsDropDown(binding.menu, 0, 0, Gravity.BOTTOM)
} }
private fun addDeleteMessageTaskToQueue(eventLog: EventLogData, position: Int) {
val task = lifecycleScope.launch {
delay(2800) // Duration of Snackbar.LENGTH_LONG
withContext(Dispatchers.Main) {
if (isActive) {
Log.i("[Chat Room] Message/event deletion task is still active, proceed")
val chatMessage = eventLog.eventLog.chatMessage
if (chatMessage != null) {
Log.i("[Chat Room] Deleting message $chatMessage at position $position")
listViewModel.deleteMessage(chatMessage)
} else {
Log.i("[Chat Room] Deleting event $eventLog at position $position")
listViewModel.deleteEventLogs(arrayListOf(eventLog))
}
viewModel.updateLastMessageToDisplay()
}
}
}
(requireActivity() as MainActivity).showSnackBar(
R.string.chat_message_removal_info,
R.string.chat_message_abort_removal
) {
Log.i("[Chat Room] Canceled message/event deletion task: $task for message/event at position $position")
adapter.notifyItemRangeChanged(position, adapter.itemCount - position)
task.cancel()
}
}
private fun scrollToFirstUnreadMessageOrBottom(smooth: Boolean) { private fun scrollToFirstUnreadMessageOrBottom(smooth: Boolean) {
if (_adapter != null && adapter.itemCount > 0) { if (_adapter != null && adapter.itemCount > 0) {
// Scroll to first unread message if any // Scroll to first unread message if any
@ -858,11 +972,13 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
{ {
dialog.dismiss() dialog.dismiss()
lifecycleScope.launch { lifecycleScope.launch {
Log.w("[Chat Room] Content is encrypted, requesting plain file path")
val plainFilePath = content.plainFilePath val plainFilePath = content.plainFilePath
Log.i("[Cht Room] Making a copy of [$plainFilePath] to the cache directory before exporting it") Log.i("[Cht Room] Making a copy of [$plainFilePath] to the cache directory before exporting it")
val cacheCopyPath = FileUtils.copyFileToCache(plainFilePath) val cacheCopyPath = FileUtils.copyFileToCache(plainFilePath)
if (cacheCopyPath != null) { if (cacheCopyPath != null) {
Log.i("[Cht Room] Cache copy has been made: $cacheCopyPath") Log.i("[Cht Room] Cache copy has been made: $cacheCopyPath")
FileUtils.deleteFile(plainFilePath)
if (!FileUtils.openFileInThirdPartyApp(requireActivity(), cacheCopyPath)) { if (!FileUtils.openFileInThirdPartyApp(requireActivity(), cacheCopyPath)) {
showDialogToSuggestOpeningFileAsText() showDialogToSuggestOpeningFileAsText()
} }

View file

@ -54,7 +54,7 @@ class DevicesFragment : SecureFragment<ChatRoomDevicesFragmentBinding>() {
return return
} }
isSecure = chatRoom.currentParams.encryptionEnabled() isSecure = chatRoom.currentParams.isEncryptionEnabled
listViewModel = ViewModelProvider( listViewModel = ViewModelProvider(
this, this,
@ -66,4 +66,10 @@ class DevicesFragment : SecureFragment<ChatRoomDevicesFragmentBinding>() {
goBack() goBack()
} }
} }
override fun onResume() {
super.onResume()
listViewModel.updateParticipants()
}
} }

View file

@ -61,7 +61,7 @@ class GroupInfoFragment : SecureFragment<ChatRoomGroupInfoFragmentBinding>() {
} }
val chatRoom: ChatRoom? = sharedViewModel.selectedGroupChatRoom.value val chatRoom: ChatRoom? = sharedViewModel.selectedGroupChatRoom.value
isSecure = chatRoom?.currentParams?.encryptionEnabled() ?: false isSecure = chatRoom?.currentParams?.isEncryptionEnabled ?: false
viewModel = ViewModelProvider( viewModel = ViewModelProvider(
this, this,
@ -84,36 +84,32 @@ class GroupInfoFragment : SecureFragment<ChatRoomGroupInfoFragmentBinding>() {
binding.participants.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager)) binding.participants.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
viewModel.participants.observe( viewModel.participants.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
adapter.submitList(it) adapter.submitList(it)
} }
)
viewModel.isMeAdmin.observe( viewModel.isMeAdmin.observe(
viewLifecycleOwner, viewLifecycleOwner
{ isMeAdmin -> ) { isMeAdmin ->
adapter.showAdminControls(isMeAdmin && chatRoom != null) adapter.showAdminControls(isMeAdmin && chatRoom != null)
} }
)
viewModel.meAdminChangedEvent.observe( viewModel.meAdminChangedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { isMeAdmin -> it.consume { isMeAdmin ->
showMeAdminStateChanged(isMeAdmin) showMeAdminStateChanged(isMeAdmin)
}
} }
) }
adapter.participantRemovedEvent.observe( adapter.participantRemovedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { participant -> it.consume { participant ->
viewModel.removeParticipant(participant) viewModel.removeParticipant(participant)
}
} }
) }
addParticipantsFromSharedViewModel() addParticipantsFromSharedViewModel()
@ -122,22 +118,20 @@ class GroupInfoFragment : SecureFragment<ChatRoomGroupInfoFragmentBinding>() {
} }
viewModel.createdChatRoomEvent.observe( viewModel.createdChatRoomEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { chatRoom -> it.consume { chatRoom ->
goToChatRoom(chatRoom, true) goToChatRoom(chatRoom, true)
}
} }
) }
viewModel.updatedChatRoomEvent.observe( viewModel.updatedChatRoomEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { chatRoom -> it.consume { chatRoom ->
goToChatRoom(chatRoom, false) goToChatRoom(chatRoom, false)
}
} }
) }
binding.setNextClickListener { binding.setNextClickListener {
if (viewModel.chatRoom != null) { if (viewModel.chatRoom != null) {
@ -182,13 +176,12 @@ class GroupInfoFragment : SecureFragment<ChatRoomGroupInfoFragmentBinding>() {
} }
viewModel.onErrorEvent.observe( viewModel.onErrorEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { messageResourceId -> it.consume { messageResourceId ->
(activity as MainActivity).showSnackBar(messageResourceId) (activity as MainActivity).showSnackBar(messageResourceId)
}
} }
) }
} }
private fun addParticipantsFromSharedViewModel() { private fun addParticipantsFromSharedViewModel() {

View file

@ -61,7 +61,7 @@ class ImdnFragment : SecureFragment<ChatRoomImdnFragmentBinding>() {
return return
} }
isSecure = chatRoom.currentParams.encryptionEnabled() isSecure = chatRoom.currentParams.isEncryptionEnabled
if (arguments != null) { if (arguments != null) {
val messageId = arguments?.getString("MessageId") val messageId = arguments?.getString("MessageId")
@ -98,11 +98,10 @@ class ImdnFragment : SecureFragment<ChatRoomImdnFragmentBinding>() {
binding.participantsList.addItemDecoration(headerItemDecoration) binding.participantsList.addItemDecoration(headerItemDecoration)
viewModel.participants.observe( viewModel.participants.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
adapter.submitList(it) adapter.submitList(it)
} }
)
binding.setBackClickListener { binding.setBackClickListener {
goBack() goBack()

View file

@ -98,7 +98,9 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
isSecure = true isSecure = true
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
listViewModel = ViewModelProvider(this)[ChatRoomsListViewModel::class.java] listViewModel = requireActivity().run {
ViewModelProvider(this)[ChatRoomsListViewModel::class.java]
}
binding.viewModel = listViewModel binding.viewModel = listViewModel
/* Shared view model & sliding pane related */ /* Shared view model & sliding pane related */
@ -111,52 +113,39 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
// Chat room loading can take some time, so wait until it is ready before opening the pane // Chat room loading can take some time, so wait until it is ready before opening the pane
sharedViewModel.chatRoomFragmentOpenedEvent.observe( sharedViewModel.chatRoomFragmentOpenedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
binding.slidingPane.openPane() binding.slidingPane.openPane()
}
} }
) }
sharedViewModel.closeSlidingPaneEvent.observe( sharedViewModel.closeSlidingPaneEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
if (!binding.slidingPane.closePane()) { (requireActivity() as MainActivity).hideKeyboard()
goBack() if (!binding.slidingPane.closePane()) {
} goBack()
} }
} }
) }
sharedViewModel.layoutChangedEvent.observe( sharedViewModel.layoutChangedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
if (binding.slidingPane.isSlideable) { if (binding.slidingPane.isSlideable) {
val navHostFragment = childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment val navHostFragment =
if (navHostFragment.navController.currentDestination?.id == R.id.emptyChatFragment) { childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment
Log.i("[Chat] Foldable device has been folded, closing side pane with empty fragment") if (navHostFragment.navController.currentDestination?.id == R.id.emptyChatFragment) {
binding.slidingPane.closePane() Log.i("[Chat] Foldable device has been folded, closing side pane with empty fragment")
} binding.slidingPane.closePane()
} }
} }
} }
) }
binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED 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) { }
override fun onPanelClosed(panel: View) {
if (binding.slidingPane.isSlideable) {
// (requireActivity() as MainActivity).showTabsFragment()
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
}
}
})
/* End of shared view model & sliding pane related */ /* End of shared view model & sliding pane related */
@ -186,9 +175,9 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
) )
val swipeListener = object : RecyclerViewSwipeListener { val swipeListener = object : RecyclerViewSwipeListener {
override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) { override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) {
val chatRoomViewModel = adapter.currentList[viewHolder.adapterPosition] val chatRoomViewModel = adapter.currentList[viewHolder.bindingAdapterPosition]
chatRoomViewModel.chatRoom.markAsRead() chatRoomViewModel.chatRoom.markAsRead()
adapter.notifyItemChanged(viewHolder.adapterPosition) adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
} }
override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) { override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) {
@ -196,13 +185,13 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel) val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
viewModel.showCancelButton { viewModel.showCancelButton {
adapter.notifyItemChanged(viewHolder.adapterPosition) adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
dialog.dismiss() dialog.dismiss()
} }
viewModel.showDeleteButton( viewModel.showDeleteButton(
{ {
val deletedChatRoom = adapter.currentList[viewHolder.adapterPosition].chatRoom val deletedChatRoom = adapter.currentList[viewHolder.bindingAdapterPosition].chatRoom
listViewModel.deleteChatRoom(deletedChatRoom) listViewModel.deleteChatRoom(deletedChatRoom)
if (!binding.slidingPane.isSlideable && if (!binding.slidingPane.isSlideable &&
deletedChatRoom == sharedViewModel.selectedChatRoom.value deletedChatRoom == sharedViewModel.selectedChatRoom.value
@ -225,43 +214,47 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
binding.chatList.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager)) binding.chatList.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
listViewModel.chatRooms.observe( listViewModel.chatRooms.observe(
viewLifecycleOwner, viewLifecycleOwner
{ chatRooms -> ) { chatRooms ->
adapter.submitList(chatRooms) adapter.submitList(chatRooms)
} }
)
listViewModel.contactsUpdatedEvent.observe( listViewModel.contactsUpdatedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
}
} }
) }
adapter.selectedChatRoomEvent.observe( adapter.selectedChatRoomEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { chatRoom -> it.consume { chatRoom ->
if ((requireActivity() as GenericActivity).isDestructionPending) { if ((requireActivity() as GenericActivity).isDestructionPending) {
Log.w("[Chat] Activity is pending destruction, don't start navigating now!") Log.w("[Chat] Activity is pending destruction, don't start navigating now!")
sharedViewModel.destructionPendingChatRoom = chatRoom sharedViewModel.destructionPendingChatRoom = chatRoom
} else { } else {
if (chatRoom.peerAddress.asStringUriOnly() == coreContext.notificationsManager.currentlyDisplayedChatRoomAddress) { if (chatRoom.peerAddress.asStringUriOnly() == coreContext.notificationsManager.currentlyDisplayedChatRoomAddress) {
Log.w("[Chat] This chat room is already displayed!") 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 { } else {
sharedViewModel.selectedChatRoom.value = chatRoom Log.w("[Chat] This chat room is already displayed!")
navigateToChatRoom(
AppUtils.createBundleWithSharedTextAndFiles(
sharedViewModel
)
)
} }
} else {
sharedViewModel.selectedChatRoom.value = chatRoom
navigateToChatRoom(
AppUtils.createBundleWithSharedTextAndFiles(
sharedViewModel
)
)
} }
} }
} }
) }
binding.setEditClickListener { binding.setEditClickListener {
listSelectionViewModel.isEditionEnabled.value = true listSelectionViewModel.isEditionEnabled.value = true
@ -315,56 +308,52 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
} }
} else { } else {
sharedViewModel.textToShare.observe( sharedViewModel.textToShare.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
Log.i("[Chat] Found text to share") Log.i("[Chat] Found text to share")
// val activity = requireActivity() as MainActivity // val activity = requireActivity() as MainActivity
// activity.showSnackBar(R.string.chat_room_toast_choose_for_sharing) // activity.showSnackBar(R.string.chat_room_toast_choose_for_sharing)
listViewModel.textSharingPending.value = true listViewModel.textSharingPending.value = true
clearDisplayedChatRoom() clearDisplayedChatRoom()
} else { } else {
if (sharedViewModel.filesToShare.value.isNullOrEmpty()) { if (sharedViewModel.filesToShare.value.isNullOrEmpty()) {
listViewModel.textSharingPending.value = false listViewModel.textSharingPending.value = false
}
} }
} }
) }
sharedViewModel.filesToShare.observe( sharedViewModel.filesToShare.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
Log.i("[Chat] Found ${it.size} files to share") Log.i("[Chat] Found ${it.size} files to share")
// val activity = requireActivity() as MainActivity // val activity = requireActivity() as MainActivity
// activity.showSnackBar(R.string.chat_room_toast_choose_for_sharing) // activity.showSnackBar(R.string.chat_room_toast_choose_for_sharing)
listViewModel.fileSharingPending.value = true listViewModel.fileSharingPending.value = true
clearDisplayedChatRoom() clearDisplayedChatRoom()
} else { } else {
if (sharedViewModel.textToShare.value.isNullOrEmpty()) { if (sharedViewModel.textToShare.value.isNullOrEmpty()) {
listViewModel.fileSharingPending.value = false listViewModel.fileSharingPending.value = false
}
} }
} }
) }
sharedViewModel.isPendingMessageForward.observe( sharedViewModel.isPendingMessageForward.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
listViewModel.forwardPending.value = it listViewModel.forwardPending.value = it
adapter.forwardPending(it) adapter.forwardPending(it)
if (it) { if (it) {
Log.i("[Chat] Found chat message to transfer") Log.i("[Chat] Found chat message to transfer")
}
} }
) }
listViewModel.onErrorEvent.observe( listViewModel.onErrorEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { messageResourceId -> it.consume { messageResourceId ->
(activity as MainActivity).showSnackBar(messageResourceId) (activity as MainActivity).showSnackBar(messageResourceId)
}
} }
) }
} }
} }

View file

@ -19,6 +19,7 @@
*/ */
package org.linphone.activities.main.chat.viewmodels package org.linphone.activities.main.chat.viewmodels
import android.view.inputmethod.EditorInfo
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -37,6 +38,7 @@ import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
import org.linphone.activities.main.chat.data.ChatMessageAttachmentData import org.linphone.activities.main.chat.data.ChatMessageAttachmentData
import org.linphone.activities.main.chat.data.ChatMessageData import org.linphone.activities.main.chat.data.ChatMessageData
import org.linphone.compatibility.Compatibility
import org.linphone.core.* import org.linphone.core.*
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils import org.linphone.utils.AppUtils
@ -86,11 +88,18 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
val isPlayingVoiceRecording = MutableLiveData<Boolean>() val isPlayingVoiceRecording = MutableLiveData<Boolean>()
val recorder: Recorder
val voiceRecordPlayingPosition = MutableLiveData<Int>() val voiceRecordPlayingPosition = MutableLiveData<Int>()
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 lateinit var voiceRecordingPlayer: Player
private val playerListener = PlayerListener { private val playerListener = PlayerListener {
@ -98,9 +107,19 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
stopVoiceRecordPlayer() 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()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
init { init {
chatRoom.addListener(chatRoomListener)
attachments.value = arrayListOf() attachments.value = arrayListOf()
attachFileEnabled.value = true attachFileEnabled.value = true
@ -118,6 +137,7 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
override fun onCleared() { override fun onCleared() {
attachments.value.orEmpty().forEach(ChatMessageAttachmentData::destroy) attachments.value.orEmpty().forEach(ChatMessageAttachmentData::destroy)
pendingChatMessageToReplyTo.value?.destroy()
if (recorder.state != RecorderState.Closed) { if (recorder.state != RecorderState.Closed) {
recorder.close() recorder.close()
@ -128,6 +148,7 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
voiceRecordingPlayer.removeListener(playerListener) voiceRecordingPlayer.removeListener(playerListener)
} }
chatRoom.removeListener(chatRoomListener)
scope.cancel() scope.cancel()
super.onCleared() super.onCleared()
} }
@ -418,8 +439,9 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
private fun initVoiceRecordPlayer() { private fun initVoiceRecordPlayer() {
Log.i("[Chat Message Sending] Creating player for voice record") 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 // If none are available, default one will be used
var headphonesCard: String? = null
var speakerCard: String? = null var speakerCard: String? = null
var earpieceCard: String? = null var earpieceCard: String? = null
for (device in coreContext.core.audioDevices) { for (device in coreContext.core.audioDevices) {
@ -428,12 +450,14 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
speakerCard = device.id speakerCard = device.id
} else if (device.type == AudioDevice.Type.Earpiece) { } else if (device.type == AudioDevice.Type.Earpiece) {
earpieceCard = device.id 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) { if (localPlayer != null) {
voiceRecordingPlayer = localPlayer voiceRecordingPlayer = localPlayer
} else { } else {

View file

@ -158,27 +158,21 @@ class ChatMessagesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
} }
fun deleteMessage(chatMessage: ChatMessage) { fun deleteMessage(chatMessage: ChatMessage) {
val position: Int = chatMessage.userData as Int
LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage) LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage)
chatRoom.deleteMessage(chatMessage) chatRoom.deleteMessage(chatMessage)
val list = arrayListOf<EventLogData>() events.value.orEmpty().forEach(EventLogData::destroy)
list.addAll(events.value.orEmpty()) events.value = getEvents()
list.removeAt(position)
events.value = list
} }
fun deleteEventLogs(listToDelete: ArrayList<EventLogData>) { fun deleteEventLogs(listToDelete: ArrayList<EventLogData>) {
val list = arrayListOf<EventLogData>()
list.addAll(events.value.orEmpty())
for (eventLog in listToDelete) { for (eventLog in listToDelete) {
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog.eventLog) LinphoneUtils.deleteFilesAttachedToEventLog(eventLog.eventLog)
eventLog.eventLog.deleteFromDatabase() eventLog.eventLog.deleteFromDatabase()
list.remove(eventLog)
} }
events.value = list events.value.orEmpty().forEach(EventLogData::destroy)
events.value = getEvents()
} }
fun loadMoreData(totalItemsCount: Int) { fun loadMoreData(totalItemsCount: Int) {
@ -248,6 +242,8 @@ class ChatMessagesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage) LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage)
chatRoom.deleteMessage(chatMessage) chatRoom.deleteMessage(chatMessage)
} }
events.value.orEmpty().forEach(EventLogData::destroy)
events.value = getEvents() events.value = getEvents()
} }
} }

View file

@ -160,9 +160,9 @@ class ChatRoomCreationViewModel : ErrorReportingViewModel() {
val encrypted = isEncrypted.value == true val encrypted = isEncrypted.value == true
val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams() val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams()
params.backend = ChatRoomBackend.Basic params.backend = ChatRoomBackend.Basic
params.enableGroup(false) params.isGroupEnabled = false
if (encrypted) { if (encrypted) {
params.enableEncryption(true) params.isEncryptionEnabled = true
params.backend = ChatRoomBackend.FlexisipChat params.backend = ChatRoomBackend.FlexisipChat
params.ephemeralMode = if (corePreferences.useEphemeralPerDeviceMode) params.ephemeralMode = if (corePreferences.useEphemeralPerDeviceMode)
ChatRoomEphemeralMode.DeviceManaged ChatRoomEphemeralMode.DeviceManaged

View file

@ -74,6 +74,8 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
val peerSipUri = MutableLiveData<String>() val peerSipUri = MutableLiveData<String>()
val ephemeralEnabled = MutableLiveData<Boolean>()
val oneToOneChatRoom: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) val oneToOneChatRoom: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())
val encryptedChatRoom: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.Encrypted.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 oneParticipantOneDevice: Boolean = false
var addressToCall: Address? = null
var onlyParticipantOnlyDeviceAddress: Address? = null var onlyParticipantOnlyDeviceAddress: Address? = null
val chatUnreadCountTranslateY = MutableLiveData<Float>() val chatUnreadCountTranslateY = MutableLiveData<Float>()
private var addressToCall: Address? = null
private val bounceAnimator: ValueAnimator by lazy { private val bounceAnimator: ValueAnimator by lazy {
ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.tabs_fragment_unread_count_bounce_offset), 0f).apply { ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.tabs_fragment_unread_count_bounce_offset), 0f).apply {
addUpdateListener { addUpdateListener {
@ -111,6 +113,7 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
override fun onContactsUpdated() { override fun onContactsUpdated() {
Log.i("[Chat Room] Contacts have changed") Log.i("[Chat Room] Contacts have changed")
contactLookup() contactLookup()
updateLastMessageToDisplay()
} }
} }
@ -196,7 +199,11 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) { override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) {
Log.i("[Chat Room] Ephemeral message deleted, updated last message displayed") 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) { override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) {
@ -209,16 +216,17 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
chatRoom.addListener(chatRoomListener) chatRoom.addListener(chatRoomListener)
coreContext.contactsManager.addListener(contactsUpdatedListener) coreContext.contactsManager.addListener(contactsUpdatedListener)
lastMessageText.value = formatLastMessage(chatRoom.lastMessageInHistory)
unreadMessagesCount.value = chatRoom.unreadMessagesCount unreadMessagesCount.value = chatRoom.unreadMessagesCount
lastUpdate.value = TimestampUtils.toString(chatRoom.lastUpdateTime, true) lastUpdate.value = TimestampUtils.toString(chatRoom.lastUpdateTime, true)
subject.value = chatRoom.subject subject.value = chatRoom.subject
updateSecurityIcon() updateSecurityIcon()
meAdmin.value = chatRoom.me?.isAdmin ?: false meAdmin.value = chatRoom.me?.isAdmin ?: false
ephemeralEnabled.value = chatRoom.isEphemeralEnabled
contactLookup() contactLookup()
updateParticipants() updateParticipants()
updateLastMessageToDisplay()
callInProgress.value = chatRoom.core.callsNb > 0 callInProgress.value = chatRoom.core.callsNb > 0
updateRemotesComposing() 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 { private fun formatLastMessage(msg: ChatMessage?): String {
if (msg == null) return "" if (msg == null) return ""

View file

@ -142,9 +142,7 @@ class ChatRoomsListViewModel : ErrorReportingViewModel() {
} }
private fun updateChatRooms() { private fun updateChatRooms() {
for (chatRoomViewModel in chatRooms.value.orEmpty()) { chatRooms.value.orEmpty().forEach(ChatRoomViewModel::destroy)
chatRoomViewModel.destroy()
}
val list = arrayListOf<ChatRoomViewModel>() val list = arrayListOf<ChatRoomViewModel>()
for (chatRoom in coreContext.core.chatRooms) { for (chatRoom in coreContext.core.chatRooms) {
@ -155,6 +153,14 @@ class ChatRoomsListViewModel : ErrorReportingViewModel() {
} }
private fun addChatRoom(chatRoom: ChatRoom) { 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<ChatRoomViewModel>() val list = arrayListOf<ChatRoomViewModel>()
val viewModel = ChatRoomViewModel(chatRoom) val viewModel = ChatRoomViewModel(chatRoom)
list.add(viewModel) list.add(viewModel)
@ -170,12 +176,10 @@ class ChatRoomsListViewModel : ErrorReportingViewModel() {
} }
private fun findChatRoomIndex(chatRoom: ChatRoom): Int { private fun findChatRoomIndex(chatRoom: ChatRoom): Int {
var index = 0 for ((index, chatRoomViewModel) in chatRooms.value.orEmpty().withIndex()) {
for (chatRoomViewModel in chatRooms.value.orEmpty()) {
if (chatRoomViewModel.chatRoom == chatRoom) { if (chatRoomViewModel.chatRoom == chatRoom) {
return index return index
} }
index++
} }
return -1 return -1
} }

View file

@ -59,16 +59,16 @@ class DevicesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
init { init {
chatRoom.addListener(listener) chatRoom.addListener(listener)
updateParticipants()
} }
override fun onCleared() { override fun onCleared() {
participants.value.orEmpty().forEach(DevicesListGroupData::destroy) participants.value.orEmpty().forEach(DevicesListGroupData::destroy)
chatRoom.removeListener(listener) chatRoom.removeListener(listener)
super.onCleared() super.onCleared()
} }
private fun updateParticipants() { fun updateParticipants() {
participants.value.orEmpty().forEach(DevicesListGroupData::destroy) participants.value.orEmpty().forEach(DevicesListGroupData::destroy)
val list = arrayListOf<DevicesListGroupData>() val list = arrayListOf<DevicesListGroupData>()

View file

@ -50,8 +50,8 @@ class EphemeralViewModel(private val chatRoom: ChatRoom) : ViewModel() {
} }
init { init {
Log.i("[Ephemeral Messages] Current lifetime is ${chatRoom.ephemeralLifetime}, ephemeral enabled? ${chatRoom.ephemeralEnabled()}") Log.i("[Ephemeral Messages] Current lifetime is ${chatRoom.ephemeralLifetime}, ephemeral enabled? ${chatRoom.isEphemeralEnabled}")
currentSelectedDuration = if (chatRoom.ephemeralEnabled()) chatRoom.ephemeralLifetime else 0 currentSelectedDuration = if (chatRoom.isEphemeralEnabled) chatRoom.ephemeralLifetime else 0
computeEphemeralDurationValues() computeEphemeralDurationValues()
} }
@ -65,13 +65,13 @@ class EphemeralViewModel(private val chatRoom: ChatRoom) : ViewModel() {
Log.i("[Ephemeral Messages] Configured lifetime for ephemeral messages was already $currentSelectedDuration") 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") 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") Log.i("[Ephemeral Messages] Ephemeral messages were enabled, disable them")
chatRoom.enableEphemeral(false) chatRoom.isEphemeralEnabled = false
} }
} }

View file

@ -117,8 +117,8 @@ class GroupInfoViewModel(val chatRoom: ChatRoom?) : ErrorReportingViewModel() {
fun createChatRoom() { fun createChatRoom() {
waitForChatRoomCreation.value = true waitForChatRoomCreation.value = true
val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams() val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams()
params.enableEncryption(isEncrypted.value == true) params.isEncryptionEnabled = isEncrypted.value == true
params.enableGroup(true) params.isGroupEnabled = true
if (isEncrypted.value == true) { if (isEncrypted.value == true) {
params.ephemeralMode = if (corePreferences.useEphemeralPerDeviceMode) params.ephemeralMode = if (corePreferences.useEphemeralPerDeviceMode)
ChatRoomEphemeralMode.DeviceManaged ChatRoomEphemeralMode.DeviceManaged

View file

@ -21,6 +21,7 @@ package org.linphone.activities.main.chat.views
import android.content.Context import android.content.Context
import android.text.Layout import android.text.Layout
import android.text.method.LinkMovementMethod
import android.util.AttributeSet import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.AppCompatTextView
import kotlin.math.ceil import kotlin.math.ceil
@ -40,6 +41,12 @@ class MultiLineWrapContentWidthTextView : AppCompatTextView {
defStyleAttr: Int defStyleAttr: Int
) : super(context, attrs, defStyleAttr) ) : 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) { override fun onMeasure(widthSpec: Int, heightSpec: Int) {
var wSpec = widthSpec var wSpec = widthSpec
val widthMode = MeasureSpec.getMode(wSpec) val widthMode = MeasureSpec.getMode(wSpec)

View file

@ -22,6 +22,7 @@ package org.linphone.activities.main.chat.views
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.KeyEvent
import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.AppCompatEditText
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.lifecycle.ViewModelProvider 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. * Allows for image input inside an EditText, usefull for keyboards with gif support for example.
*/ */
class RichEditText : AppCompatEditText { class RichEditText : AppCompatEditText {
private var controlPressed = false
private var sendListener: RichEditTextSendListener? = null
constructor(context: Context) : super(context) { constructor(context: Context) : super(context) {
initReceiveContentListener() initReceiveContentListener()
} }
@ -51,6 +56,10 @@ class RichEditText : AppCompatEditText {
initReceiveContentListener() initReceiveContentListener()
} }
fun setControlEnterListener(listener: RichEditTextSendListener) {
sendListener = listener
}
private fun initReceiveContentListener() { private fun initReceiveContentListener() {
ViewCompat.setOnReceiveContentListener( ViewCompat.setOnReceiveContentListener(
this, RichContentReceiver.MIME_TYPES, this, RichContentReceiver.MIME_TYPES,
@ -63,5 +72,25 @@ class RichEditText : AppCompatEditText {
sharedViewModel.richContentUri.value = Event(uri) 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()
}

View file

@ -71,15 +71,14 @@ class ContactsListAdapter(
// This is for item selection through ListTopBarFragment // This is for item selection through ListTopBarFragment
selectionListViewModel = selectionViewModel selectionListViewModel = selectionViewModel
selectionViewModel.isEditionEnabled.observe( selectionViewModel.isEditionEnabled.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
position = adapterPosition position = bindingAdapterPosition
} }
)
setClickListener { setClickListener {
if (selectionViewModel.isEditionEnabled.value == true) { if (selectionViewModel.isEditionEnabled.value == true) {
selectionViewModel.onToggleSelect(adapterPosition) selectionViewModel.onToggleSelect(bindingAdapterPosition)
} else { } else {
selectedContactEvent.value = Event(contactViewModel.contactInternal) selectedContactEvent.value = Event(contactViewModel.contactInternal)
} }

View file

@ -83,47 +83,50 @@ class DetailContactFragment : GenericFragment<ContactDetailFragmentBinding>() {
binding.viewModel = viewModel binding.viewModel = viewModel
viewModel.sendSmsToEvent.observe( viewModel.sendSmsToEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { number -> it.consume { number ->
sendSms(number) sendSms(number)
}
} }
) }
viewModel.startCallToEvent.observe( viewModel.startCallToEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { address -> it.consume { address ->
if (coreContext.core.callsNb > 0) { if (coreContext.core.callsNb > 0) {
Log.i("[Contact] Starting dialer with pre-filled URI ${address.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}") Log.i("[Contact] Starting dialer with pre-filled URI ${address.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}")
sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event(R.id.dialerFragment) sharedViewModel.updateContactsAnimationsBasedOnDestination.value =
sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.masterContactsFragment) Event(R.id.dialerFragment)
sharedViewModel.updateDialerAnimationsBasedOnDestination.value =
Event(R.id.masterContactsFragment)
val args = Bundle() val args = Bundle()
args.putString("URI", address.asStringUriOnly()) args.putString("URI", address.asStringUriOnly())
args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer) args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer)
args.putBoolean("SkipAutoCallStart", true) // If auto start call setting is enabled, ignore it args.putBoolean(
navigateToDialer(args) "SkipAutoCallStart",
} else { true
coreContext.startCall(address) ) // If auto start call setting is enabled, ignore it
} navigateToDialer(args)
} else {
coreContext.startCall(address)
} }
} }
) }
viewModel.chatRoomCreatedEvent.observe( viewModel.chatRoomCreatedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { chatRoom -> it.consume { chatRoom ->
sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event(R.id.masterChatRoomsFragment) sharedViewModel.updateContactsAnimationsBasedOnDestination.value =
val args = Bundle() Event(R.id.masterChatRoomsFragment)
args.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly()) val args = Bundle()
args.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly()) args.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly())
navigateToChatRoom(args) args.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly())
} navigateToChatRoom(args)
} }
) }
binding.setBackClickListener { binding.setBackClickListener {
goBack() goBack()
@ -138,13 +141,12 @@ class DetailContactFragment : GenericFragment<ContactDetailFragmentBinding>() {
} }
viewModel.onErrorEvent.observe( viewModel.onErrorEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { messageResourceId -> it.consume { messageResourceId ->
(activity as MainActivity).showSnackBar(messageResourceId) (activity as MainActivity).showSnackBar(messageResourceId)
}
} }
) }
view.doOnPreDraw { view.doOnPreDraw {
// Notifies fragment is ready to be drawn // Notifies fragment is ready to be drawn

View file

@ -85,75 +85,59 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
useMaterialSharedAxisXForwardAnimation = false useMaterialSharedAxisXForwardAnimation = false
sharedViewModel.updateContactsAnimationsBasedOnDestination.observe( sharedViewModel.updateContactsAnimationsBasedOnDestination.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { id -> it.consume { id ->
val forward = when (id) { val forward = when (id) {
R.id.dialerFragment, R.id.masterChatRoomsFragment -> false R.id.dialerFragment, R.id.masterChatRoomsFragment -> false
else -> true else -> true
} }
if (corePreferences.enableAnimations) { if (corePreferences.enableAnimations) {
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE val portraitOrientation =
val axis = if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
enterTransition = MaterialSharedAxis(axis, forward) val axis =
reenterTransition = MaterialSharedAxis(axis, forward) if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y
returnTransition = MaterialSharedAxis(axis, !forward) enterTransition = MaterialSharedAxis(axis, forward)
exitTransition = MaterialSharedAxis(axis, !forward) reenterTransition = MaterialSharedAxis(axis, forward)
} returnTransition = MaterialSharedAxis(axis, !forward)
exitTransition = MaterialSharedAxis(axis, !forward)
} }
} }
) }
sharedViewModel.contactFragmentOpenedEvent.observe( sharedViewModel.contactFragmentOpenedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
binding.slidingPane.openPane() binding.slidingPane.openPane()
}
} }
) }
sharedViewModel.closeSlidingPaneEvent.observe( sharedViewModel.closeSlidingPaneEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
if (!binding.slidingPane.closePane()) { if (!binding.slidingPane.closePane()) {
goBack() goBack()
}
} }
} }
) }
sharedViewModel.layoutChangedEvent.observe( sharedViewModel.layoutChangedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
if (binding.slidingPane.isSlideable) { if (binding.slidingPane.isSlideable) {
val navHostFragment = childFragmentManager.findFragmentById(R.id.contacts_nav_container) as NavHostFragment val navHostFragment =
if (navHostFragment.navController.currentDestination?.id == R.id.emptyContactFragment) { childFragmentManager.findFragmentById(R.id.contacts_nav_container) as NavHostFragment
Log.i("[Contacts] Foldable device has been folded, closing side pane with empty fragment") if (navHostFragment.navController.currentDestination?.id == R.id.emptyContactFragment) {
binding.slidingPane.closePane() 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.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 */ /* End of shared view model & sliding pane related */
@ -190,13 +174,13 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel) val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
viewModel.showCancelButton { viewModel.showCancelButton {
adapter.notifyItemChanged(viewHolder.adapterPosition) adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
dialog.dismiss() dialog.dismiss()
} }
viewModel.showDeleteButton( viewModel.showDeleteButton(
{ {
val deletedContact = adapter.currentList[viewHolder.adapterPosition].contactInternal val deletedContact = adapter.currentList[viewHolder.bindingAdapterPosition].contactInternal
listViewModel.deleteContact(deletedContact) listViewModel.deleteContact(deletedContact)
if (!binding.slidingPane.isSlideable && if (!binding.slidingPane.isSlideable &&
deletedContact == sharedViewModel.selectedContact.value deletedContact == sharedViewModel.selectedContact.value
@ -223,38 +207,36 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
binding.contactsList.addItemDecoration(headerItemDecoration) binding.contactsList.addItemDecoration(headerItemDecoration)
adapter.selectedContactEvent.observe( adapter.selectedContactEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { contact -> it.consume { contact ->
Log.i("[Contacts] Selected item in list changed: $contact") Log.i("[Contacts] Selected item in list changed: $contact")
sharedViewModel.selectedContact.value = contact sharedViewModel.selectedContact.value = contact
if (editOnClick) { if (editOnClick) {
navigateToContactEditor(sipUriToAdd, binding.slidingPane) navigateToContactEditor(sipUriToAdd, binding.slidingPane)
editOnClick = false editOnClick = false
sipUriToAdd = null sipUriToAdd = null
} else { } else {
navigateToContact() navigateToContact()
}
} }
} }
) }
listViewModel.contactsList.observe( listViewModel.contactsList.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
val id = contactIdToDisplay val id = contactIdToDisplay
if (id != null) { if (id != null) {
val contact = coreContext.contactsManager.findContactById(id) val contact = coreContext.contactsManager.findContactById(id)
if (contact != null) { if (contact != null) {
contactIdToDisplay = null contactIdToDisplay = null
Log.i("[Contacts] Found matching contact $contact after callback") Log.i("[Contacts] Found matching contact $contact after callback")
adapter.selectedContactEvent.value = Event(contact) adapter.selectedContactEvent.value = Event(contact)
}
} }
adapter.submitList(it)
} }
) adapter.submitList(it)
}
binding.setAllContactsToggleClickListener { binding.setAllContactsToggleClickListener {
listViewModel.sipContactsSelected.value = false listViewModel.sipContactsSelected.value = false
@ -264,18 +246,16 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
} }
listViewModel.sipContactsSelected.observe( listViewModel.sipContactsSelected.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
listViewModel.updateContactsList() listViewModel.updateContactsList()
} }
)
listViewModel.filter.observe( listViewModel.filter.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
listViewModel.updateContactsList() listViewModel.updateContactsList()
} }
)
binding.setNewContactClickListener { binding.setNewContactClickListener {
// Remove any previously selected contact // Remove any previously selected contact

View file

@ -45,9 +45,11 @@ import org.linphone.activities.main.viewmodels.DialogViewModel
import org.linphone.activities.main.viewmodels.SharedMainViewModel import org.linphone.activities.main.viewmodels.SharedMainViewModel
import org.linphone.activities.navigateToConfigFileViewer import org.linphone.activities.navigateToConfigFileViewer
import org.linphone.activities.navigateToContacts import org.linphone.activities.navigateToContacts
import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.databinding.DialerFragmentBinding import org.linphone.databinding.DialerFragmentBinding
import org.linphone.mediastream.Version import org.linphone.mediastream.Version
import org.linphone.telecom.TelecomHelper
import org.linphone.utils.AppUtils import org.linphone.utils.AppUtils
import org.linphone.utils.DialogUtils import org.linphone.utils.DialogUtils
import org.linphone.utils.Event import org.linphone.utils.Event
@ -75,24 +77,25 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
useMaterialSharedAxisXForwardAnimation = false useMaterialSharedAxisXForwardAnimation = false
sharedViewModel.updateDialerAnimationsBasedOnDestination.observe( sharedViewModel.updateDialerAnimationsBasedOnDestination.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { id -> it.consume { id ->
val forward = when (id) { val forward = when (id) {
R.id.masterChatRoomsFragment -> false R.id.masterChatRoomsFragment -> false
else -> true else -> true
} }
if (corePreferences.enableAnimations) { if (corePreferences.enableAnimations) {
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE val portraitOrientation =
val axis = if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
enterTransition = MaterialSharedAxis(axis, forward) val axis =
reenterTransition = MaterialSharedAxis(axis, forward) if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y
returnTransition = MaterialSharedAxis(axis, !forward) enterTransition = MaterialSharedAxis(axis, forward)
exitTransition = MaterialSharedAxis(axis, !forward) reenterTransition = MaterialSharedAxis(axis, forward)
} returnTransition = MaterialSharedAxis(axis, !forward)
exitTransition = MaterialSharedAxis(axis, !forward)
} }
} }
) }
binding.setNewContactClickListener { binding.setNewContactClickListener {
sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.masterContactsFragment) sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.masterContactsFragment)
@ -108,6 +111,47 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
} }
} }
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) { if (arguments?.containsKey("Transfer") == true) {
sharedViewModel.pendingCallTransfer = arguments?.getBoolean("Transfer") ?: false sharedViewModel.pendingCallTransfer = arguments?.getBoolean("Transfer") ?: false
Log.i("[Dialer] Is pending call transfer: ${sharedViewModel.pendingCallTransfer}") Log.i("[Dialer] Is pending call transfer: ${sharedViewModel.pendingCallTransfer}")
@ -127,45 +171,6 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
} }
arguments?.clear() 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}") Log.i("[Dialer] Pending call transfer mode = ${sharedViewModel.pendingCallTransfer}")
viewModel.transferVisibility.value = sharedViewModel.pendingCallTransfer viewModel.transferVisibility.value = sharedViewModel.pendingCallTransfer
@ -205,18 +210,72 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
Log.i("[Dialer] READ_PHONE_STATE permission has been granted") Log.i("[Dialer] READ_PHONE_STATE permission has been granted")
coreContext.initPhoneStateListener() 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) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
} }
@TargetApi(Version.API23_MARSHMALLOW_60) @TargetApi(Version.API23_MARSHMALLOW_60)
private fun checkPermissions() { 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()) { if (!PermissionHelper.get().hasReadPhoneStatePermission()) {
Log.i("[Dialer] Asking for READ_PHONE_STATE permission") Log.i("[Dialer] Asking for READ_PHONE_STATE permission")
requestPermissions(arrayOf(Manifest.permission.READ_PHONE_STATE), 0) 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() { private fun displayDebugPopup() {
val alertDialog = MaterialAlertDialogBuilder(requireContext()) val alertDialog = MaterialAlertDialogBuilder(requireContext())
alertDialog.setTitle(getString(R.string.debug_popup_title)) alertDialog.setTitle(getString(R.string.debug_popup_title))

View file

@ -21,7 +21,6 @@ package org.linphone.activities.main.dialer.viewmodels
import android.content.Context import android.content.Context
import android.os.Vibrator import android.os.Vibrator
import android.provider.Settings
import android.text.Editable import android.text.Editable
import android.widget.EditText import android.widget.EditText
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@ -69,23 +68,8 @@ class DialerViewModel : LogsUploadViewModel() {
} }
enteredUri.value = sb.toString() enteredUri.value = sb.toString()
if (coreContext.core.callsNb == 0) { if (vibrator.hasVibrator() && corePreferences.dtmfKeypadVibration) {
val contentResolver = coreContext.context.contentResolver Compatibility.eventVibration(vibrator)
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")
}
} }
} }
@ -176,7 +160,7 @@ class DialerViewModel : LogsUploadViewModel() {
fun updateShowVideoPreview() { fun updateShowVideoPreview() {
val videoPreview = corePreferences.videoPreview val videoPreview = corePreferences.videoPreview
showPreview.value = videoPreview showPreview.value = videoPreview
coreContext.core.enableVideoPreview(videoPreview) coreContext.core.isVideoPreviewEnabled = videoPreview
} }
fun eraseLastChar() { fun eraseLastChar() {

View file

@ -56,4 +56,8 @@ abstract class GenericViewerFragment<T : ViewDataBinding> : SecureFragment<T>()
(childFragmentManager.findFragmentById(R.id.top_bar_fragment) as? TopBarFragment) (childFragmentManager.findFragmentById(R.id.top_bar_fragment) as? TopBarFragment)
?.setContent(content) ?.setContent(content)
} }
override fun goBack() {
findNavController().popBackStack()
}
} }

View file

@ -21,13 +21,21 @@ package org.linphone.activities.main.files.fragments
import android.os.Bundle import android.os.Bundle
import android.view.View 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.R
import org.linphone.activities.GenericFragment import org.linphone.activities.GenericFragment
import org.linphone.activities.SnackBarActivity import org.linphone.activities.SnackBarActivity
import org.linphone.compatibility.Compatibility
import org.linphone.core.Content import org.linphone.core.Content
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.databinding.FileViewerTopBarFragmentBinding import org.linphone.databinding.FileViewerTopBarFragmentBinding
import org.linphone.mediastream.Version
import org.linphone.utils.FileUtils import org.linphone.utils.FileUtils
import org.linphone.utils.PermissionHelper
class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() { class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
private var content: Content? = null private var content: Content? = null
@ -46,20 +54,9 @@ class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
} }
binding.setExportClickListener { binding.setExportClickListener {
if (content != null) { val contentToExport = content
val filePath = content?.plainFilePath.orEmpty() if (contentToExport != null) {
plainFilePath = if (filePath.isEmpty()) content?.filePath.orEmpty() else filePath exportContent(contentToExport)
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 { } else {
Log.e("[File Viewer] No Content set!") Log.e("[File Viewer] No Content set!")
} }
@ -89,4 +86,77 @@ class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
content = c content = c
binding.fileName.text = c.name 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)
}
}
}
}
} }

View file

@ -29,7 +29,12 @@ open class FileViewerViewModel(val content: Content) : ViewModel() {
private val deleteAfterUse: Boolean = content.isFileEncrypted private val deleteAfterUse: Boolean = content.isFileEncrypted
init { 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() { override fun onCleared() {

View file

@ -56,56 +56,55 @@ abstract class MasterFragment<T : ViewDataBinding, U : SelectionListAdapter<*, *
listSelectionViewModel = ViewModelProvider(this)[ListTopBarViewModel::class.java] listSelectionViewModel = ViewModelProvider(this)[ListTopBarViewModel::class.java]
listSelectionViewModel.isEditionEnabled.observe( listSelectionViewModel.isEditionEnabled.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
if (!it) listSelectionViewModel.onUnSelectAll() if (!it) listSelectionViewModel.onUnSelectAll()
} }
)
listSelectionViewModel.selectAllEvent.observe( listSelectionViewModel.selectAllEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
listSelectionViewModel.onSelectAll(getItemCount() - 1) listSelectionViewModel.onSelectAll(getItemCount() - 1)
}
} }
) }
listSelectionViewModel.unSelectAllEvent.observe( listSelectionViewModel.unSelectAllEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
listSelectionViewModel.onUnSelectAll() listSelectionViewModel.onUnSelectAll()
}
} }
) }
listSelectionViewModel.deleteSelectionEvent.observe( listSelectionViewModel.deleteSelectionEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
val confirmationDialog = AppUtils.getStringWithPlural(dialogConfirmationMessageBeforeRemoval, listSelectionViewModel.selectedItems.value.orEmpty().size) val confirmationDialog = AppUtils.getStringWithPlural(
val viewModel = DialogViewModel(confirmationDialog) dialogConfirmationMessageBeforeRemoval,
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel) listSelectionViewModel.selectedItems.value.orEmpty().size
)
val viewModel = DialogViewModel(confirmationDialog)
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
viewModel.showCancelButton { viewModel.showCancelButton {
dialog.dismiss()
listSelectionViewModel.isEditionEnabled.value = false
}
viewModel.showDeleteButton(
{
delete()
dialog.dismiss() dialog.dismiss()
listSelectionViewModel.isEditionEnabled.value = false listSelectionViewModel.isEditionEnabled.value = false
} },
getString(R.string.dialog_delete)
)
viewModel.showDeleteButton( dialog.show()
{
delete()
dialog.dismiss()
listSelectionViewModel.isEditionEnabled.value = false
},
getString(R.string.dialog_delete)
)
dialog.show()
}
} }
) }
} }
private fun delete() { private fun delete() {

View file

@ -51,15 +51,14 @@ class StatusFragment : GenericFragment<StatusFragmentBinding>() {
} }
sharedViewModel.accountRemoved.observe( sharedViewModel.accountRemoved.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
Log.i("[Status Fragment] An account was removed, update default account state") Log.i("[Status Fragment] An account was removed, update default account state")
val defaultAccount = coreContext.core.defaultAccount val defaultAccount = coreContext.core.defaultAccount
if (defaultAccount != null) { if (defaultAccount != null) {
viewModel.updateDefaultAccountRegistrationStatus(defaultAccount.state) viewModel.updateDefaultAccountRegistrationStatus(defaultAccount.state)
}
} }
) }
binding.setMenuClickListener { binding.setMenuClickListener {
sharedViewModel.toggleDrawerEvent.value = Event(true) sharedViewModel.toggleDrawerEvent.value = Event(true)

View file

@ -65,7 +65,7 @@ class CallLogsListAdapter(
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(callLogGroup: GroupedCallLogData) { fun bind(callLogGroup: GroupedCallLogData) {
with(binding) { with(binding) {
val callLogViewModel = callLogGroup.lastCallLogViewModel val callLogViewModel = callLogGroup.lastCallLogData
viewModel = callLogViewModel viewModel = callLogViewModel
lifecycleOwner = viewLifecycleOwner lifecycleOwner = viewLifecycleOwner
@ -73,15 +73,14 @@ class CallLogsListAdapter(
// This is for item selection through ListTopBarFragment // This is for item selection through ListTopBarFragment
selectionListViewModel = selectionViewModel selectionListViewModel = selectionViewModel
selectionViewModel.isEditionEnabled.observe( selectionViewModel.isEditionEnabled.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
position = adapterPosition position = bindingAdapterPosition
} }
)
setClickListener { setClickListener {
if (selectionViewModel.isEditionEnabled.value == true) { if (selectionViewModel.isEditionEnabled.value == true) {
selectionViewModel.onToggleSelect(adapterPosition) selectionViewModel.onToggleSelect(bindingAdapterPosition)
} else { } else {
startCallToEvent.value = Event(callLogGroup) startCallToEvent.value = Event(callLogGroup)
} }

View file

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

View file

@ -19,15 +19,14 @@
*/ */
package org.linphone.activities.main.history.data package org.linphone.activities.main.history.data
import org.linphone.activities.main.history.viewmodels.CallLogViewModel
import org.linphone.core.CallLog import org.linphone.core.CallLog
class GroupedCallLogData(callLog: CallLog) { class GroupedCallLogData(callLog: CallLog) {
var lastCallLog: CallLog = callLog var lastCallLog: CallLog = callLog
val callLogs = arrayListOf(callLog) val callLogs = arrayListOf(callLog)
val lastCallLogViewModel = CallLogViewModel(lastCallLog) val lastCallLogData = CallLogData(lastCallLog)
fun destroy() { fun destroy() {
lastCallLogViewModel.destroy() lastCallLogData.destroy()
} }
} }

View file

@ -70,7 +70,7 @@ class DetailCallLogFragment : GenericFragment<HistoryDetailFragmentBinding>() {
useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false
viewModel.relatedCallLogs.value = callLogGroup.callLogs viewModel.addRelatedCallLogs(callLogGroup.callLogs)
binding.setBackClickListener { binding.setBackClickListener {
goBack() goBack()
@ -99,50 +99,48 @@ class DetailCallLogFragment : GenericFragment<HistoryDetailFragmentBinding>() {
} }
viewModel.startCallEvent.observe( viewModel.startCallEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { callLog -> it.consume { callLog ->
val address = callLog.remoteAddress val address = callLog.remoteAddress
if (coreContext.core.callsNb > 0) { if (coreContext.core.callsNb > 0) {
Log.i("[History] Starting dialer with pre-filled URI ${address.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}") Log.i("[History] Starting dialer with pre-filled URI ${address.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}")
sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.masterCallLogsFragment) sharedViewModel.updateDialerAnimationsBasedOnDestination.value =
Event(R.id.masterCallLogsFragment)
val args = Bundle() val args = Bundle()
args.putString("URI", address.asStringUriOnly()) args.putString("URI", address.asStringUriOnly())
args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer) args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer)
args.putBoolean( args.putBoolean(
"SkipAutoCallStart", "SkipAutoCallStart",
true true
) // If auto start call setting is enabled, ignore it ) // If auto start call setting is enabled, ignore it
navigateToDialer(args) navigateToDialer(args)
} else { } else {
val localAddress = callLog.localAddress val localAddress = callLog.localAddress
coreContext.startCall(address, localAddress = localAddress) coreContext.startCall(address, localAddress = localAddress)
}
} }
} }
) }
viewModel.chatRoomCreatedEvent.observe( viewModel.chatRoomCreatedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { chatRoom -> it.consume { chatRoom ->
val args = Bundle() val args = Bundle()
args.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly()) args.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly())
args.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly()) args.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly())
navigateToChatRoom(args) navigateToChatRoom(args)
}
} }
) }
viewModel.onErrorEvent.observe( viewModel.onErrorEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { messageResourceId -> it.consume { messageResourceId ->
(activity as MainActivity).showSnackBar(messageResourceId) (activity as MainActivity).showSnackBar(messageResourceId)
}
} }
) }
} }
override fun goBack() { override fun goBack() {

View file

@ -101,46 +101,30 @@ class MasterCallLogsFragment : MasterFragment<HistoryMasterFragmentBinding, Call
view.doOnPreDraw { sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable } view.doOnPreDraw { sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable }
sharedViewModel.closeSlidingPaneEvent.observe( sharedViewModel.closeSlidingPaneEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
if (!binding.slidingPane.closePane()) { if (!binding.slidingPane.closePane()) {
goBack() goBack()
}
} }
} }
) }
sharedViewModel.layoutChangedEvent.observe( sharedViewModel.layoutChangedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
if (binding.slidingPane.isSlideable) { if (binding.slidingPane.isSlideable) {
val navHostFragment = childFragmentManager.findFragmentById(R.id.history_nav_container) as NavHostFragment val navHostFragment =
if (navHostFragment.navController.currentDestination?.id == R.id.emptyCallHistoryFragment) { childFragmentManager.findFragmentById(R.id.history_nav_container) as NavHostFragment
Log.i("[History] Foldable device has been folded, closing side pane with empty fragment") if (navHostFragment.navController.currentDestination?.id == R.id.emptyCallHistoryFragment) {
binding.slidingPane.closePane() Log.i("[History] Foldable device has been folded, closing side pane with empty fragment")
} binding.slidingPane.closePane()
} }
} }
} }
) }
binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED 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 */ /* End of shared view model & sliding pane related */
@ -175,13 +159,13 @@ class MasterCallLogsFragment : MasterFragment<HistoryMasterFragmentBinding, Call
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel) val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
viewModel.showCancelButton { viewModel.showCancelButton {
adapter.notifyItemChanged(viewHolder.adapterPosition) adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
dialog.dismiss() dialog.dismiss()
} }
viewModel.showDeleteButton( viewModel.showDeleteButton(
{ {
val deletedCallGroup = adapter.currentList[viewHolder.adapterPosition] val deletedCallGroup = adapter.currentList[viewHolder.bindingAdapterPosition]
listViewModel.deleteCallLogGroup(deletedCallGroup) listViewModel.deleteCallLogGroup(deletedCallGroup)
if (!binding.slidingPane.isSlideable && if (!binding.slidingPane.isSlideable &&
deletedCallGroup.lastCallLog.callId == sharedViewModel.selectedCallLogGroup.value?.lastCallLog?.callId deletedCallGroup.lastCallLog.callId == sharedViewModel.selectedCallLogGroup.value?.lastCallLog?.callId
@ -208,73 +192,69 @@ class MasterCallLogsFragment : MasterFragment<HistoryMasterFragmentBinding, Call
binding.callLogsList.addItemDecoration(headerItemDecoration) binding.callLogsList.addItemDecoration(headerItemDecoration)
listViewModel.callLogs.observe( listViewModel.callLogs.observe(
viewLifecycleOwner, viewLifecycleOwner
{ callLogs -> ) { callLogs ->
if (listViewModel.missedCallLogsSelected.value == false) { if (listViewModel.missedCallLogsSelected.value == false) {
adapter.submitList(callLogs) adapter.submitList(callLogs)
}
} }
) }
listViewModel.missedCallLogs.observe( listViewModel.missedCallLogs.observe(
viewLifecycleOwner, viewLifecycleOwner
{ callLogs -> ) { callLogs ->
if (listViewModel.missedCallLogsSelected.value == true) { if (listViewModel.missedCallLogsSelected.value == true) {
adapter.submitList(callLogs) adapter.submitList(callLogs)
}
} }
) }
listViewModel.missedCallLogsSelected.observe( listViewModel.missedCallLogsSelected.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
if (it) { if (it) {
adapter.submitList(listViewModel.missedCallLogs.value) adapter.submitList(listViewModel.missedCallLogs.value)
} else { } else {
adapter.submitList(listViewModel.callLogs.value) adapter.submitList(listViewModel.callLogs.value)
}
} }
) }
listViewModel.contactsUpdatedEvent.observe( listViewModel.contactsUpdatedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()
}
} }
) }
adapter.selectedCallLogEvent.observe( adapter.selectedCallLogEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { callLog -> it.consume { callLog ->
sharedViewModel.selectedCallLogGroup.value = callLog sharedViewModel.selectedCallLogGroup.value = callLog
navigateToCallHistory(binding.slidingPane) navigateToCallHistory(binding.slidingPane)
}
} }
) }
adapter.startCallToEvent.observe( adapter.startCallToEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { callLogGroup -> it.consume { callLogGroup ->
val remoteAddress = callLogGroup.lastCallLog.remoteAddress val remoteAddress = callLogGroup.lastCallLog.remoteAddress
if (coreContext.core.callsNb > 0) { if (coreContext.core.callsNb > 0) {
Log.i("[History] Starting dialer with pre-filled URI ${remoteAddress.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}") Log.i("[History] Starting dialer with pre-filled URI ${remoteAddress.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}")
sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.masterCallLogsFragment) sharedViewModel.updateDialerAnimationsBasedOnDestination.value =
val args = Bundle() Event(R.id.masterCallLogsFragment)
args.putString("URI", remoteAddress.asStringUriOnly()) val args = Bundle()
args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer) args.putString("URI", remoteAddress.asStringUriOnly())
args.putBoolean("SkipAutoCallStart", true) // If auto start call setting is enabled, ignore it args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer)
navigateToDialer(args) // If auto start call setting is enabled, ignore it
} else { args.putBoolean("SkipAutoCallStart", true)
val localAddress = callLogGroup.lastCallLog.localAddress navigateToDialer(args)
coreContext.startCall(remoteAddress, localAddress = localAddress) } else {
} val localAddress = callLogGroup.lastCallLog.localAddress
coreContext.startCall(remoteAddress, localAddress = localAddress)
} }
} }
) }
binding.setAllCallLogsToggleClickListener { binding.setAllCallLogsToggleClickListener {
listViewModel.missedCallLogsSelected.value = false listViewModel.missedCallLogsSelected.value = false

View file

@ -22,17 +22,16 @@ package org.linphone.activities.main.history.viewmodels
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
import org.linphone.activities.main.history.data.CallLogData
import org.linphone.contact.GenericContactViewModel import org.linphone.contact.GenericContactViewModel
import org.linphone.core.* import org.linphone.core.*
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.utils.Event import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils import org.linphone.utils.LinphoneUtils
import org.linphone.utils.TimestampUtils
class CallLogViewModelFactory(private val callLog: CallLog) : class CallLogViewModelFactory(private val callLog: CallLog) :
ViewModelProvider.NewInstanceFactory() { ViewModelProvider.NewInstanceFactory() {
@ -48,53 +47,6 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
LinphoneUtils.getDisplayableAddress(callLog.remoteAddress) 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<Event<CallLog>> by lazy { val startCallEvent: MutableLiveData<Event<CallLog>> by lazy {
MutableLiveData<Event<CallLog>>() MutableLiveData<Event<CallLog>>()
} }
@ -109,7 +61,16 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
val secureChatAllowed = contact.value?.friend?.getPresenceModelForUriOrTel(peerSipUri)?.hasCapability(FriendCapability.LimeX3Dh) ?: false val secureChatAllowed = contact.value?.friend?.getPresenceModelForUriOrTel(peerSipUri)?.hasCapability(FriendCapability.LimeX3Dh) ?: false
val relatedCallLogs = MutableLiveData<ArrayList<CallLog>>() val relatedCallLogs = MutableLiveData<ArrayList<CallLogData>>()
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() { private val chatRoomListener = object : ChatRoomListenerStub() {
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) { override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
@ -126,14 +87,19 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
init { init {
waitForChatRoomCreation.value = false waitForChatRoomCreation.value = false
coreContext.core.addListener(listener)
} }
override fun onCleared() { override fun onCleared() {
coreContext.core.removeListener(listener)
destroy() destroy()
super.onCleared() super.onCleared()
} }
fun destroy() { fun destroy() {
relatedCallLogs.value.orEmpty().forEach(CallLogData::destroy)
} }
fun startCall() { fun startCall() {
@ -157,11 +123,15 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
} }
} }
fun getCallsHistory(): ArrayList<CallLogViewModel> { fun addRelatedCallLogs(logs: ArrayList<CallLog>) {
val callsHistory = ArrayList<CallLogViewModel>() val callsHistory = ArrayList<CallLogData>()
for (callLog in relatedCallLogs.value.orEmpty()) {
callsHistory.add(CallLogViewModel(callLog)) // 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
} }
} }

View file

@ -69,12 +69,12 @@ class RecordingsListAdapter(
lifecycleOwner = viewLifecycleOwner lifecycleOwner = viewLifecycleOwner
// This is for item selection through ListTopBarFragment // This is for item selection through ListTopBarFragment
position = adapterPosition position = bindingAdapterPosition
selectionListViewModel = selectionViewModel selectionListViewModel = selectionViewModel
setClickListener { setClickListener {
if (selectionViewModel.isEditionEnabled.value == true) { if (selectionViewModel.isEditionEnabled.value == true) {
selectionViewModel.onToggleSelect(adapterPosition) selectionViewModel.onToggleSelect(bindingAdapterPosition)
} }
} }

View file

@ -164,8 +164,9 @@ class RecordingData(val path: String, private val recordingListener: RecordingLi
} }
private fun initPlayer() { 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 // If none are available, default one will be used
var headphonesCard: String? = null
var speakerCard: String? = null var speakerCard: String? = null
var earpieceCard: String? = null var earpieceCard: String? = null
for (device in coreContext.core.audioDevices) { for (device in coreContext.core.audioDevices) {
@ -174,11 +175,14 @@ class RecordingData(val path: String, private val recordingListener: RecordingLi
speakerCard = device.id speakerCard = device.id
} else if (device.type == AudioDevice.Type.Earpiece) { } else if (device.type == AudioDevice.Type.Earpiece) {
earpieceCard = device.id 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 if (localPlayer != null) player = localPlayer
else Log.e("[Recording VM] Couldn't create local player!") else Log.e("[Recording VM] Couldn't create local player!")
player.addListener(listener) player.addListener(listener)

View file

@ -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.adapters.RecordingsListAdapter
import org.linphone.activities.main.recordings.data.RecordingData import org.linphone.activities.main.recordings.data.RecordingData
import org.linphone.activities.main.recordings.viewmodels.RecordingsViewModel import org.linphone.activities.main.recordings.viewmodels.RecordingsViewModel
import org.linphone.core.tools.Log
import org.linphone.databinding.RecordingsFragmentBinding import org.linphone.databinding.RecordingsFragmentBinding
import org.linphone.utils.AppUtils import org.linphone.utils.AppUtils
import org.linphone.utils.RecyclerViewHeaderDecoration import org.linphone.utils.RecyclerViewHeaderDecoration
@ -69,11 +70,10 @@ class RecordingsFragment : MasterFragment<RecordingsFragmentBinding, RecordingsL
binding.recordingsList.addItemDecoration(headerItemDecoration) binding.recordingsList.addItemDecoration(headerItemDecoration)
viewModel.recordingsList.observe( viewModel.recordingsList.observe(
viewLifecycleOwner, viewLifecycleOwner
{ recordings -> ) { recordings ->
adapter.submitList(recordings) adapter.submitList(recordings)
} }
)
binding.setBackClickListener { goBack() } binding.setBackClickListener { goBack() }
@ -111,4 +111,13 @@ class RecordingsFragment : MasterFragment<RecordingsFragmentBinding, RecordingsL
} }
viewModel.deleteRecordings(list) viewModel.deleteRecordings(list)
} }
override fun onResume() {
if (this::viewModel.isInitialized) {
viewModel.udpdateRecordingsList()
} else {
Log.e("[Recordings] Fragment resuming but viewModel lateinit property isn't initialized!")
}
super.onResume()
}
} }

View file

@ -59,7 +59,6 @@ class RecordingsViewModel : ViewModel() {
} }
init { init {
getRecordings()
isVideoVisible.value = false isVideoVisible.value = false
} }
@ -86,10 +85,10 @@ class RecordingsViewModel : ViewModel() {
FileUtils.deleteFile(recording.path) FileUtils.deleteFile(recording.path)
} }
getRecordings() udpdateRecordingsList()
} }
private fun getRecordings() { fun udpdateRecordingsList() {
recordingsList.value.orEmpty().forEach(RecordingData::destroy) recordingsList.value.orEmpty().forEach(RecordingData::destroy)
val list = arrayListOf<RecordingData>() val list = arrayListOf<RecordingData>()

View file

@ -63,32 +63,30 @@ class AccountSettingsFragment : GenericSettingFragment<SettingsAccountFragmentBi
binding.setBackClickListener { goBack() } binding.setBackClickListener { goBack() }
viewModel.linkPhoneNumberEvent.observe( viewModel.linkPhoneNumberEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
val authInfo = viewModel.account.findAuthInfo() val authInfo = viewModel.account.findAuthInfo()
if (authInfo == null) { if (authInfo == null) {
Log.e("[Account Settings] Failed to find auth info for account ${viewModel.account}") Log.e("[Account Settings] Failed to find auth info for account ${viewModel.account}")
} else { } else {
val args = Bundle() val args = Bundle()
args.putString("Username", authInfo.username) args.putString("Username", authInfo.username)
args.putString("Password", authInfo.password) args.putString("Password", authInfo.password)
args.putString("HA1", authInfo.ha1) args.putString("HA1", authInfo.ha1)
navigateToPhoneLinking(args) navigateToPhoneLinking(args)
}
} }
} }
) }
viewModel.accountRemovedEvent.observe( viewModel.accountRemovedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
sharedViewModel.accountRemoved.value = true sharedViewModel.accountRemoved.value = true
goBack() goBack()
}
} }
) }
view.doOnPreDraw { view.doOnPreDraw {
// Notifies fragment is ready to be drawn // Notifies fragment is ready to be drawn

View file

@ -55,103 +55,96 @@ class AdvancedSettingsFragment : GenericSettingFragment<SettingsAdvancedFragment
binding.setBackClickListener { goBack() } binding.setBackClickListener { goBack() }
viewModel.uploadFinishedEvent.observe( viewModel.uploadFinishedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { url -> it.consume { url ->
val clipboard = val clipboard =
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Logs url", url) val clip = ClipData.newPlainText("Logs url", url)
clipboard.setPrimaryClip(clip) clipboard.setPrimaryClip(clip)
val activity = requireActivity() as MainActivity val activity = requireActivity() as MainActivity
activity.showSnackBar(R.string.logs_url_copied_to_clipboard) activity.showSnackBar(R.string.logs_url_copied_to_clipboard)
AppUtils.shareUploadedLogsUrl(activity, url) AppUtils.shareUploadedLogsUrl(activity, url)
}
} }
) }
viewModel.uploadErrorEvent.observe( viewModel.uploadErrorEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
val activity = requireActivity() as MainActivity val activity = requireActivity() as MainActivity
activity.showSnackBar(R.string.logs_upload_failure) activity.showSnackBar(R.string.logs_upload_failure)
}
} }
) }
viewModel.resetCompleteEvent.observe( viewModel.resetCompleteEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
val activity = requireActivity() as MainActivity val activity = requireActivity() as MainActivity
activity.showSnackBar(R.string.logs_reset_complete) activity.showSnackBar(R.string.logs_reset_complete)
}
} }
) }
viewModel.setNightModeEvent.observe( viewModel.setNightModeEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { value -> it.consume { value ->
AppCompatDelegate.setDefaultNightMode( AppCompatDelegate.setDefaultNightMode(
when (value) { when (value) {
0 -> AppCompatDelegate.MODE_NIGHT_NO 0 -> AppCompatDelegate.MODE_NIGHT_NO
1 -> AppCompatDelegate.MODE_NIGHT_YES 1 -> AppCompatDelegate.MODE_NIGHT_YES
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
} }
) )
}
} }
) }
viewModel.backgroundModeEnabled.value = !DeviceUtils.isAppUserRestricted(requireContext()) viewModel.backgroundModeEnabled.value = !DeviceUtils.isAppUserRestricted(requireContext())
viewModel.goToBatterySettingsEvent.observe( viewModel.goToBatterySettingsEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
try { try {
val intent = Intent("android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS") val intent = Intent("android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS")
startActivity(intent) startActivity(intent)
} catch (anfe: ActivityNotFoundException) { } catch (anfe: ActivityNotFoundException) {
Log.e("[Advanced Settings] ActivityNotFound exception: ", anfe) Log.e("[Advanced Settings] ActivityNotFound exception: ", anfe)
}
} }
} }
) }
viewModel.powerManagerSettingsVisibility.value = PowerManagerUtils.getDevicePowerManagerIntent(requireContext()) != null viewModel.powerManagerSettingsVisibility.value = PowerManagerUtils.getDevicePowerManagerIntent(requireContext()) != null
viewModel.goToPowerManagerSettingsEvent.observe( viewModel.goToPowerManagerSettingsEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
val intent = PowerManagerUtils.getDevicePowerManagerIntent(requireActivity()) val intent = PowerManagerUtils.getDevicePowerManagerIntent(requireActivity())
if (intent != null) { if (intent != null) {
try { try {
startActivity(intent) startActivity(intent)
} catch (se: SecurityException) { } catch (se: SecurityException) {
Log.e("[Advanced Settings] Security exception: ", se) Log.e("[Advanced Settings] Security exception: ", se)
}
} }
} }
} }
) }
viewModel.goToAndroidSettingsEvent.observe( viewModel.goToAndroidSettingsEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
val intent = Intent() val intent = Intent()
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
intent.addCategory(Intent.CATEGORY_DEFAULT) intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.data = Uri.parse("package:${requireContext().packageName}") intent.data = Uri.parse("package:${requireContext().packageName}")
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
ContextCompat.startActivity(requireContext(), intent, null) ContextCompat.startActivity(requireContext(), intent, null)
}
} }
) }
} }
override fun goBack() { override fun goBack() {

View file

@ -54,24 +54,22 @@ class AudioSettingsFragment : GenericSettingFragment<SettingsAudioFragmentBindin
binding.setBackClickListener { goBack() } binding.setBackClickListener { goBack() }
viewModel.askAudioRecordPermissionForEchoCancellerCalibrationEvent.observe( viewModel.askAudioRecordPermissionForEchoCancellerCalibrationEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
Log.i("[Audio Settings] Asking for RECORD_AUDIO permission for echo canceller calibration") Log.i("[Audio Settings] Asking for RECORD_AUDIO permission for echo canceller calibration")
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 1) requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 1)
}
} }
) }
viewModel.askAudioRecordPermissionForEchoTesterEvent.observe( viewModel.askAudioRecordPermissionForEchoTesterEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
Log.i("[Audio Settings] Asking for RECORD_AUDIO permission for echo tester") Log.i("[Audio Settings] Asking for RECORD_AUDIO permission for echo tester")
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 2) requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 2)
}
} }
) }
initAudioCodecsList() initAudioCodecsList()

View file

@ -19,7 +19,6 @@
*/ */
package org.linphone.activities.main.settings.fragments package org.linphone.activities.main.settings.fragments
import android.Manifest
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
@ -38,7 +37,6 @@ import org.linphone.databinding.SettingsCallFragmentBinding
import org.linphone.mediastream.Version import org.linphone.mediastream.Version
import org.linphone.telecom.TelecomHelper import org.linphone.telecom.TelecomHelper
import org.linphone.utils.Event import org.linphone.utils.Event
import org.linphone.utils.PermissionHelper
class CallSettingsFragment : GenericSettingFragment<SettingsCallFragmentBinding>() { class CallSettingsFragment : GenericSettingFragment<SettingsCallFragmentBinding>() {
private lateinit var viewModel: CallSettingsViewModel private lateinit var viewModel: CallSettingsViewModel
@ -57,80 +55,75 @@ class CallSettingsFragment : GenericSettingFragment<SettingsCallFragmentBinding>
binding.setBackClickListener { goBack() } binding.setBackClickListener { goBack() }
viewModel.systemWideOverlayEnabledEvent.observe( viewModel.systemWideOverlayEnabledEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
if (!Compatibility.canDrawOverlay(requireContext())) { if (!Compatibility.canDrawOverlay(requireContext())) {
val intent = Intent("android.settings.action.MANAGE_OVERLAY_PERMISSION", Uri.parse("package:${requireContext().packageName}")) val intent = Intent(
startActivityForResult(intent, 0) "android.settings.action.MANAGE_OVERLAY_PERMISSION",
} Uri.parse("package:${requireContext().packageName}")
)
startActivityForResult(intent, 0)
} }
} }
) }
viewModel.goToAndroidNotificationSettingsEvent.observe( viewModel.goToAndroidNotificationSettingsEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
if (Build.VERSION.SDK_INT >= Version.API26_O_80) { if (Build.VERSION.SDK_INT >= Version.API26_O_80) {
val i = Intent() val i = Intent()
i.action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS i.action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
i.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) i.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
i.putExtra( i.putExtra(
Settings.EXTRA_CHANNEL_ID, Settings.EXTRA_CHANNEL_ID,
getString(R.string.notification_channel_service_id) getString(R.string.notification_channel_service_id)
) )
i.addCategory(Intent.CATEGORY_DEFAULT) i.addCategory(Intent.CATEGORY_DEFAULT)
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
startActivity(i) startActivity(i)
}
} }
} }
) }
viewModel.enableTelecomManagerEvent.observe( viewModel.enableTelecomManagerEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
if (!PermissionHelper.get().hasTelecomManagerPermissions()) { if (!Compatibility.hasTelecomManagerPermissions(requireContext())) {
val permissions = arrayOf( Compatibility.requestTelecomManagerPermissions(requireActivity(), 1)
Manifest.permission.READ_PHONE_NUMBERS, } else if (!TelecomHelper.exists()) {
Manifest.permission.MANAGE_OWN_CALLS corePreferences.useTelecomManager = true
) Log.w("[Telecom Helper] Doesn't exists yet, creating it")
requestPermissions(permissions, 1) TelecomHelper.create(requireContext())
} else if (!TelecomHelper.exists()) { updateTelecomManagerAccount()
corePreferences.useTelecomManager = true
Log.w("[Telecom Helper] Doesn't exists yet, creating it")
TelecomHelper.create(requireContext())
updateTelecomManagerAccount()
}
} }
} }
) }
viewModel.goToAndroidNotificationSettingsEvent.observe( viewModel.goToAndroidNotificationSettingsEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
if (Build.VERSION.SDK_INT >= Version.API26_O_80) { if (Build.VERSION.SDK_INT >= Version.API26_O_80) {
val i = Intent() val i = Intent()
i.action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS i.action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
i.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) i.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
i.putExtra( i.putExtra(
Settings.EXTRA_CHANNEL_ID, Settings.EXTRA_CHANNEL_ID,
getString(R.string.notification_channel_service_id) getString(R.string.notification_channel_service_id)
) )
i.addCategory(Intent.CATEGORY_DEFAULT) i.addCategory(Intent.CATEGORY_DEFAULT)
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
startActivity(i) startActivity(i)
}
} }
} }
) }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

View file

@ -50,39 +50,37 @@ class ChatSettingsFragment : GenericSettingFragment<SettingsChatFragmentBinding>
binding.setBackClickListener { goBack() } binding.setBackClickListener { goBack() }
viewModel.launcherShortcutsEvent.observe( viewModel.launcherShortcutsEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { newValue -> it.consume { newValue ->
if (newValue) { if (newValue) {
Compatibility.createShortcutsToChatRooms(requireContext()) Compatibility.createShortcutsToChatRooms(requireContext())
} else { } else {
Compatibility.removeShortcuts(requireContext()) Compatibility.removeShortcuts(requireContext())
}
} }
} }
) }
viewModel.goToAndroidNotificationSettingsEvent.observe( viewModel.goToAndroidNotificationSettingsEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
if (Build.VERSION.SDK_INT >= Version.API26_O_80) { if (Build.VERSION.SDK_INT >= Version.API26_O_80) {
val i = Intent() val i = Intent()
i.action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS i.action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
i.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) i.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName)
i.putExtra( i.putExtra(
Settings.EXTRA_CHANNEL_ID, Settings.EXTRA_CHANNEL_ID,
getString(R.string.notification_channel_chat_id) getString(R.string.notification_channel_chat_id)
) )
i.addCategory(Intent.CATEGORY_DEFAULT) i.addCategory(Intent.CATEGORY_DEFAULT)
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
startActivity(i) startActivity(i)
}
} }
} }
) }
} }
override fun goBack() { override fun goBack() {

View file

@ -51,30 +51,28 @@ class ContactsSettingsFragment : GenericSettingFragment<SettingsContactsFragment
binding.setBackClickListener { goBack() } binding.setBackClickListener { goBack() }
viewModel.launcherShortcutsEvent.observe( viewModel.launcherShortcutsEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { newValue -> it.consume { newValue ->
if (newValue) { if (newValue) {
Compatibility.createShortcutsToContacts(requireContext()) Compatibility.createShortcutsToContacts(requireContext())
} else { } else {
Compatibility.removeShortcuts(requireContext()) Compatibility.removeShortcuts(requireContext())
if (corePreferences.chatRoomShortcuts) { if (corePreferences.chatRoomShortcuts) {
Compatibility.createShortcutsToChatRooms(requireContext()) Compatibility.createShortcutsToChatRooms(requireContext())
}
} }
} }
} }
) }
viewModel.askWriteContactsPermissionForPresenceStorageEvent.observe( viewModel.askWriteContactsPermissionForPresenceStorageEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
Log.i("[Contacts Settings] Asking for WRITE_CONTACTS permission to be able to store presence") Log.i("[Contacts Settings] Asking for WRITE_CONTACTS permission to be able to store presence")
requestPermissions(arrayOf(android.Manifest.permission.WRITE_CONTACTS), 1) requestPermissions(arrayOf(android.Manifest.permission.WRITE_CONTACTS), 1)
}
} }
) }
if (!PermissionHelper.required(requireContext()).hasReadContactsPermission()) { if (!PermissionHelper.required(requireContext()).hasReadContactsPermission()) {
Log.i("[Contacts Settings] Asking for READ_CONTACTS permission") Log.i("[Contacts Settings] Asking for READ_CONTACTS permission")

View file

@ -69,39 +69,37 @@ class SettingsFragment : SecureFragment<SettingsFragmentBinding>() {
// Account settings loading can take some time, so wait until it is ready before opening the pane // Account settings loading can take some time, so wait until it is ready before opening the pane
sharedViewModel.accountSettingsFragmentOpenedEvent.observe( sharedViewModel.accountSettingsFragmentOpenedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
binding.slidingPane.openPane() binding.slidingPane.openPane()
}
} }
) }
sharedViewModel.closeSlidingPaneEvent.observe( sharedViewModel.closeSlidingPaneEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
if (!binding.slidingPane.closePane()) { if (!binding.slidingPane.closePane()) {
goBack() goBack()
}
} }
} }
) }
sharedViewModel.layoutChangedEvent.observe( sharedViewModel.layoutChangedEvent.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
it.consume { it.consume {
sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
if (binding.slidingPane.isSlideable) { if (binding.slidingPane.isSlideable) {
val navHostFragment = childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment val navHostFragment =
if (navHostFragment.navController.currentDestination?.id == R.id.emptySettingsFragment) { childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment
Log.i("[Settings] Foldable device has been folded, closing side pane with empty fragment") if (navHostFragment.navController.currentDestination?.id == R.id.emptySettingsFragment) {
binding.slidingPane.closePane() Log.i("[Settings] Foldable device has been folded, closing side pane with empty fragment")
} binding.slidingPane.closePane()
} }
} }
} }
) }
binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
/* End of shared view model & sliding pane related */ /* End of shared view model & sliding pane related */
@ -112,12 +110,11 @@ class SettingsFragment : SecureFragment<SettingsFragmentBinding>() {
binding.setBackClickListener { goBack() } binding.setBackClickListener { goBack() }
sharedViewModel.accountRemoved.observe( sharedViewModel.accountRemoved.observe(
viewLifecycleOwner, viewLifecycleOwner
{ ) {
Log.i("[Settings] Account removed, update accounts list") Log.i("[Settings] Account removed, update accounts list")
viewModel.updateAccountsList() viewModel.updateAccountsList()
} }
)
val identity = arguments?.getString("Identity") val identity = arguments?.getString("Identity")
if (identity != null) { if (identity != null) {

View file

@ -192,7 +192,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
val disableListener = object : SettingListenerStub() { val disableListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) { override fun onBoolValueChanged(newValue: Boolean) {
val params = account.params.clone() val params = account.params.clone()
params.registerEnabled = !newValue params.isRegisterEnabled = !newValue
account.params = params account.params = params
} }
} }
@ -239,7 +239,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
} }
val params = account.params.clone() val params = account.params.clone()
params.registerEnabled = false params.isRegisterEnabled = false
account.params = params account.params = params
if (!registered) { if (!registered) {
@ -288,7 +288,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
val outboundProxyListener = object : SettingListenerStub() { val outboundProxyListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) { override fun onBoolValueChanged(newValue: Boolean) {
val params = account.params.clone() val params = account.params.clone()
params.outboundProxyEnabled = newValue params.isOutboundProxyEnabled = newValue
account.params = params account.params = params
} }
} }
@ -297,8 +297,15 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
val stunServerListener = object : SettingListenerStub() { val stunServerListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) { override fun onTextValueChanged(newValue: String) {
val params = account.params.clone() val params = account.params.clone()
params.natPolicy?.stunServer = newValue if (params.natPolicy == null) {
if (newValue.isEmpty()) ice.value = false 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 stunServer.value = newValue
account.params = params account.params = params
} }
@ -308,7 +315,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
val iceListener = object : SettingListenerStub() { val iceListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) { override fun onBoolValueChanged(newValue: Boolean) {
val params = account.params.clone() val params = account.params.clone()
params.natPolicy?.enableIce(newValue) params.natPolicy?.isIceEnabled = newValue
account.params = params account.params = params
} }
} }
@ -370,7 +377,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
val escapePlusListener = object : SettingListenerStub() { val escapePlusListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) { override fun onBoolValueChanged(newValue: Boolean) {
val params = account.params.clone() val params = account.params.clone()
params.dialEscapePlusEnabled = newValue params.isDialEscapePlusEnabled = newValue
account.params = params account.params = params
} }
} }
@ -424,19 +431,19 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
userName.value = params.identityAddress?.username userName.value = params.identityAddress?.username
userId.value = account.findAuthInfo()?.userid userId.value = account.findAuthInfo()?.userid
domain.value = params.identityAddress?.domain domain.value = params.identityAddress?.domain
disable.value = !params.registerEnabled disable.value = !params.isRegisterEnabled
pushNotification.value = params.pushNotificationAllowed pushNotification.value = params.pushNotificationAllowed
pushNotificationsAvailable.value = core.isPushNotificationAvailable pushNotificationsAvailable.value = core.isPushNotificationAvailable
proxy.value = params.serverAddress?.asStringUriOnly() proxy.value = params.serverAddress?.asStringUriOnly()
outboundProxy.value = params.outboundProxyEnabled outboundProxy.value = params.isOutboundProxyEnabled
stunServer.value = params.natPolicy?.stunServer stunServer.value = params.natPolicy?.stunServer
ice.value = params.natPolicy?.iceEnabled() ice.value = params.natPolicy?.isIceEnabled
avpf.value = params.avpfMode == AVPFMode.Enabled avpf.value = params.avpfMode == AVPFMode.Enabled
avpfRrInterval.value = params.avpfRrInterval avpfRrInterval.value = params.avpfRrInterval
expires.value = params.expires expires.value = params.expires
prefix.value = params.internationalPrefix prefix.value = params.internationalPrefix
dialPrefix.value = params.useInternationalPrefixForCallsAndChats dialPrefix.value = params.useInternationalPrefixForCallsAndChats
escapePlus.value = params.dialEscapePlusEnabled escapePlus.value = params.isDialEscapePlusEnabled
} }
private fun initTransportList() { private fun initTransportList() {

View file

@ -32,8 +32,8 @@ import org.linphone.mediastream.Version
import org.linphone.utils.Event import org.linphone.utils.Event
class AdvancedSettingsViewModel : LogsUploadViewModel() { class AdvancedSettingsViewModel : LogsUploadViewModel() {
protected val prefs = corePreferences private val prefs = corePreferences
protected val core = coreContext.core private val core = coreContext.core
val debugModeListener = object : SettingListenerStub() { val debugModeListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) { override fun onBoolValueChanged(newValue: Boolean) {

View file

@ -41,7 +41,7 @@ class AudioSettingsViewModel : GenericSettingsViewModel() {
val echoCancellationListener = object : SettingListenerStub() { val echoCancellationListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) { override fun onBoolValueChanged(newValue: Boolean) {
core.enableEchoCancellation(newValue) core.isEchoCancellationEnabled = newValue
} }
} }
val echoCancellation = MutableLiveData<Boolean>() val echoCancellation = MutableLiveData<Boolean>()
@ -81,7 +81,7 @@ class AudioSettingsViewModel : GenericSettingsViewModel() {
val adaptiveRateControlListener = object : SettingListenerStub() { val adaptiveRateControlListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) { override fun onBoolValueChanged(newValue: Boolean) {
core.enableAdaptiveRateControl(newValue) core.isAdaptiveRateControlEnabled = newValue
} }
} }
val adaptiveRateControl = MutableLiveData<Boolean>() val adaptiveRateControl = MutableLiveData<Boolean>()
@ -110,6 +110,13 @@ class AudioSettingsViewModel : GenericSettingsViewModel() {
val outputAudioDeviceLabels = MutableLiveData<ArrayList<String>>() val outputAudioDeviceLabels = MutableLiveData<ArrayList<String>>()
private val outputAudioDeviceValues = MutableLiveData<ArrayList<AudioDevice>>() private val outputAudioDeviceValues = MutableLiveData<ArrayList<AudioDevice>>()
val preferBluetoothDevicesListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) {
prefs.routeAudioToBluetoothIfAvailable = newValue
}
}
val preferBluetoothDevices = MutableLiveData<Boolean>()
val codecBitrateListener = object : SettingListenerStub() { val codecBitrateListener = object : SettingListenerStub() {
override fun onListValueChanged(position: Int) { override fun onListValueChanged(position: Int) {
for (payloadType in core.audioPayloadTypes) { for (payloadType in core.audioPayloadTypes) {
@ -146,14 +153,15 @@ class AudioSettingsViewModel : GenericSettingsViewModel() {
val audioCodecs = MutableLiveData<ArrayList<ViewDataBinding>>() val audioCodecs = MutableLiveData<ArrayList<ViewDataBinding>>()
init { init {
echoCancellation.value = core.echoCancellationEnabled() echoCancellation.value = core.isEchoCancellationEnabled
adaptiveRateControl.value = core.adaptiveRateControlEnabled() adaptiveRateControl.value = core.isAdaptiveRateControlEnabled
echoCalibration.value = if (core.echoCancellationEnabled()) { echoCalibration.value = if (core.isEchoCancellationEnabled) {
prefs.getString(R.string.audio_settings_echo_cancellation_calibration_value).format(prefs.echoCancellerCalibration) prefs.getString(R.string.audio_settings_echo_cancellation_calibration_value).format(prefs.echoCancellerCalibration)
} else { } else {
prefs.getString(R.string.audio_settings_echo_canceller_calibration_summary) prefs.getString(R.string.audio_settings_echo_canceller_calibration_summary)
} }
echoTesterStatus.value = prefs.getString(R.string.audio_settings_echo_tester_summary) echoTesterStatus.value = prefs.getString(R.string.audio_settings_echo_tester_summary)
preferBluetoothDevices.value = prefs.routeAudioToBluetoothIfAvailable
initInputAudioDevicesList() initInputAudioDevicesList()
initOutputAudioDevicesList() initOutputAudioDevicesList()
initCodecBitrateList() initCodecBitrateList()

View file

@ -20,22 +20,40 @@
package org.linphone.activities.main.settings.viewmodels package org.linphone.activities.main.settings.viewmodels
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import java.io.File
import java.util.*
import kotlin.collections.ArrayList
import org.linphone.R import org.linphone.R
import org.linphone.activities.main.settings.SettingListenerStub import org.linphone.activities.main.settings.SettingListenerStub
import org.linphone.core.MediaEncryption import org.linphone.core.MediaEncryption
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.mediastream.Version import org.linphone.mediastream.Version
import org.linphone.telecom.TelecomHelper import org.linphone.telecom.TelecomHelper
import org.linphone.utils.AppUtils
import org.linphone.utils.Event import org.linphone.utils.Event
class CallSettingsViewModel : GenericSettingsViewModel() { class CallSettingsViewModel : GenericSettingsViewModel() {
val deviceRingtoneListener = object : SettingListenerStub() { val deviceRingtoneListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) { override fun onBoolValueChanged(newValue: Boolean) {
core.ring = if (newValue) null else prefs.ringtonePath core.ring = if (newValue) null else prefs.defaultRingtonePath
} }
} }
val deviceRingtone = MutableLiveData<Boolean>() val deviceRingtone = MutableLiveData<Boolean>()
val ringtoneListener = object : SettingListenerStub() {
override fun onListValueChanged(position: Int) {
if (position == 0) {
core.ring = null
} else {
core.ring = ringtoneValues[position]
}
}
}
val ringtoneIndex = MutableLiveData<Int>()
val ringtoneLabels = MutableLiveData<ArrayList<String>>()
private val ringtoneValues = arrayListOf<String>()
val showRingtonesList = MutableLiveData<Boolean>()
val vibrateOnIncomingCallListener = object : SettingListenerStub() { val vibrateOnIncomingCallListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) { override fun onBoolValueChanged(newValue: Boolean) {
core.isVibrationOnIncomingCallEnabled = newValue core.isVibrationOnIncomingCallEnabled = newValue
@ -73,6 +91,9 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
TelecomHelper.get().removeAccount() TelecomHelper.get().removeAccount()
TelecomHelper.get().destroy() TelecomHelper.get().destroy()
TelecomHelper.destroy() TelecomHelper.destroy()
Log.w("[Call Settings] Disabling Telecom Manager auto-enable")
prefs.manuallyDisabledTelecomManager = true
} }
prefs.useTelecomManager = newValue prefs.useTelecomManager = newValue
} }
@ -209,7 +230,10 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
val goToAndroidNotificationSettingsEvent = MutableLiveData<Event<Boolean>>() val goToAndroidNotificationSettingsEvent = MutableLiveData<Event<Boolean>>()
init { init {
initRingtonesList()
deviceRingtone.value = core.ring == null deviceRingtone.value = core.ring == null
showRingtonesList.value = prefs.showAllRingtones
vibrateOnIncomingCall.value = core.isVibrationOnIncomingCallEnabled vibrateOnIncomingCall.value = core.isVibrationOnIncomingCallEnabled
initEncryptionList() initEncryptionList()
@ -235,6 +259,28 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
pauseCallsWhenAudioFocusIsLost.value = prefs.pauseCallsWhenAudioFocusIsLost pauseCallsWhenAudioFocusIsLost.value = prefs.pauseCallsWhenAudioFocusIsLost
} }
private fun initRingtonesList() {
val labels = arrayListOf<String>()
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() { private fun initEncryptionList() {
val labels = arrayListOf<String>() val labels = arrayListOf<String>()

View file

@ -35,7 +35,7 @@ class ChatSettingsViewModel : GenericSettingsViewModel() {
val fileSharingUrlListener = object : SettingListenerStub() { val fileSharingUrlListener = object : SettingListenerStub() {
override fun onTextValueChanged(newValue: String) { override fun onTextValueChanged(newValue: String) {
core.logCollectionUploadServerUrl = newValue core.fileTransferServer = newValue
} }
} }
val fileSharingUrl = MutableLiveData<String>() val fileSharingUrl = MutableLiveData<String>()

View file

@ -33,7 +33,7 @@ class ContactsSettingsViewModel : GenericSettingsViewModel() {
val friendListSubscribeListener = object : SettingListenerStub() { val friendListSubscribeListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) { override fun onBoolValueChanged(newValue: Boolean) {
core.enableFriendListSubscription(newValue) core.isFriendListSubscriptionEnabled = newValue
} }
} }
val friendListSubscribe = MutableLiveData<Boolean>() val friendListSubscribe = MutableLiveData<Boolean>()

View file

@ -26,14 +26,14 @@ import org.linphone.activities.main.settings.SettingListenerStub
class NetworkSettingsViewModel : GenericSettingsViewModel() { class NetworkSettingsViewModel : GenericSettingsViewModel() {
val wifiOnlyListener = object : SettingListenerStub() { val wifiOnlyListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) { override fun onBoolValueChanged(newValue: Boolean) {
core.enableWifiOnly(newValue) core.isWifiOnlyEnabled = newValue
} }
} }
val wifiOnly = MutableLiveData<Boolean>() val wifiOnly = MutableLiveData<Boolean>()
val allowIpv6Listener = object : SettingListenerStub() { val allowIpv6Listener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) { override fun onBoolValueChanged(newValue: Boolean) {
core.enableIpv6(newValue) core.isIpv6Enabled = newValue
} }
} }
val allowIpv6 = MutableLiveData<Boolean>() val allowIpv6 = MutableLiveData<Boolean>()
@ -59,8 +59,8 @@ class NetworkSettingsViewModel : GenericSettingsViewModel() {
val sipPort = MutableLiveData<Int>() val sipPort = MutableLiveData<Int>()
init { init {
wifiOnly.value = core.wifiOnlyEnabled() wifiOnly.value = core.isWifiOnlyEnabled
allowIpv6.value = core.ipv6Enabled() allowIpv6.value = core.isIpv6Enabled
randomPorts.value = getTransportPort() == -1 randomPorts.value = getTransportPort() == -1
sipPort.value = getTransportPort() sipPort.value = getTransportPort()
} }

View file

@ -52,7 +52,7 @@ class TunnelSettingsViewModel : GenericSettingsViewModel() {
val useDualModeListener = object : SettingListenerStub() { val useDualModeListener = object : SettingListenerStub() {
override fun onBoolValueChanged(newValue: Boolean) { override fun onBoolValueChanged(newValue: Boolean) {
val tunnel = core.tunnel val tunnel = core.tunnel
tunnel?.enableDualMode(newValue) tunnel?.isDualModeEnabled = newValue
} }
} }
val useDualMode = MutableLiveData<Boolean>() val useDualMode = MutableLiveData<Boolean>()
@ -96,7 +96,7 @@ class TunnelSettingsViewModel : GenericSettingsViewModel() {
hostnameUrl.value = config.host hostnameUrl.value = config.host
port.value = config.port port.value = config.port
useDualMode.value = tunnel?.dualModeEnabled() useDualMode.value = tunnel?.isDualModeEnabled
hostnameUrl2.value = config.host2 hostnameUrl2.value = config.host2
port2.value = config.port2 port2.value = config.port2

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