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/release
app/releaseAppBundle
app/releaseWithCrashlytics
keystore.properties
app/src/main/res/xml/contacts.xml

View file

@ -7,6 +7,7 @@ job-android:
before_script:
- if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then eval $(ssh-agent -s); fi
- if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then echo "$SCP_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null; fi
- echo "$ANDROID_SETTINGS_GRADLE" > settings.gradle
script:
- sdkmanager

View file

@ -10,33 +10,58 @@ Group changes to describe their impact on the project, as follows:
Fixed for any bug fixes.
Security to invite users to upgrade in case of vulnerabilities.
## [4.6.0] - Unreleased
## [4.7.0] - Unreleased
## [4.6.0] - 2022-02-09
### Added
- Reply to chat message feature (with original message preview)
- Swipe action on chat messages to reply / delete
- Voice recordings in chat feature
- Allow video recording in chat file sharing
- Unread messages indicator in chat conversation that separates read & unread messages
- Notify incoming/outgoing calls on bluetooth devices using self-managed connections from telecom manager API
- Notify incoming/outgoing calls on bluetooth devices using self-managed connections from telecom manager API (disables SDK audio focus)
- Ask Android to not process what user types in an encrypted chat room to improve privacy, see [IME_FLAG_NO_PERSONALIZED_LEARNING](https://developer.android.com/reference/android/view/inputmethod/EditorInfo#IME_FLAG_NO_PERSONALIZED_LEARNING)
- SIP URIs in chat messages are clickable to easily initiate a call
- New video call UI on foldable device like Galaxy Z Fold
- Setting to automatically record all calls
- When using a physical keyboard, use left control + enter keys to send message
- Using CallStyle notifications for calls for devices running Android 12 or newer
- New fragment explaining generic SIP account limitations contrary to sip.linphone.org SIP accounts
- Link to Weblate added in about page
### Changed
- UI has been reworked around SlidingPane component to better handle tablets & foldable devices
- No longer scroll to bottom of chat room when new messages are received, a new button shows up to do it and it displays conversation's unread messages count
- Animations have been replaced to use com.google.android.material.transition ones
- Using new [Unified Content API](https://developer.android.com/about/versions/12/features/unified-content-api) to share files from keyboard (or other sources)
- Received messages are now trimmed
- Bumped dependencies, gradle updated from 4.2.2 to 7.0.2
- Target Android SDK version set to 31 (Android 12)
- Splashscreen is using new APIs
- SDK updated to 5.1.0 release
- Updated translations
### Fixed
- Chat notifications disappearing when app restarts
- "Infinite backstack", now each view is stored (at most) once in the backstack
- Voice messages / call recordings will be played on headset/headphones instead of speaker, if possible
- Going back to the dialer when pressing back in a chat room after clicking on a chat message notification
- Missing international prefix / phone number in assistant after granting permission
- Display issue for incoming call notification preventing to use answer/hangup actions on some Xiaomi devices (like Redmi Note 9S)
- Missing foreground service notification for background mode
### Removed
- Launcher Activity has been replaced by [Splash Screen API](https://developer.android.com/reference/kotlin/androidx/core/splashscreen/SplashScreen)
- Dialer will no longer make DTMF sound when pressing digits
- Launcher activity
- Global push notification setting in Network, use the switch in each Account instead
- No longer need to monitor device rotation and give information to the Core, it does it by itself
## [4.5.6] - 2021-11-08
### Changed
- SDK updated to 5.0.49
## [4.5.5] - 2021-10-28
@ -79,16 +104,11 @@ Group changes to describe their impact on the project, as follows:
- Fixed various crashes & other issues
- SDK bumped to 5.0.10
## [4.5.1] - Unreleased
### Added
- Reply to chat message feature
- Voice recordings messages
## [4.5.1] - 2021-07-15
### Changed
- Navigation was reworked using SlidingPane widget, reducing code & improving UI on foldables
### Removed
- Bugs & crashes have been fixed
- SDK bumped to 5.0.1
## [4.5.0] - 2021-07-08

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.
## Troubleshouting
## Troubleshooting
### Behavior issue
When submitting an issue on our [Github repository](https://github.com/BelledonneCommunications/linphone-android), please follow the template and attach the matching library logs:
@ -105,7 +107,32 @@ When submitting an issue on our [Github repository](https://github.com/Belledonn
2. Then restart the app, reproduce the issue and upload the logs using the `Send logs` button on the About page.
3. Finally paste the link to the uploaded logs (link is already in the clipboard after a sucessful upload).
3. Finally paste the link to the uploaded logs (link is already in the clipboard after a successful upload).
### Native crash
First of all, to be able to get a symbolized stack trace, you need the debug version of our libraries.
If you haven't built the SDK locally (see [building a local SDK](#BuildingalocalSDK)), here's how to get them:
1. Go to our [maven repository](https://download.linphone.org/maven_repository/org/linphone/linphone-sdk-android-debug/), in the linphone-android-debug directory.
2. Download the AAR file with **the exact same version** as the AAR that was used to generate the crash's stacktrace.
3. Extract the AAR somewhere on your computer (it's a simple ZIP file even it's doesn't have the extension). Libraries are stored inside the ```jni``` folder (a directory for each architectured built, usually ```arm64-v8a, armeabi-v7a, x86_64 and x86```).
4. To get consistent with locally built SDK, rename the ```jni``` directory into ```libs-debug```.
Now you need the ```ndk-stack``` tool and possibly ```adb logcat```.
If your computer isn't used for Android development, you can download those tools from [Google website](https://developer.android.com/studio#downloads), in the ```Command line tools only``` section.
Once you have the debug libraries and the proper tools installed, you can use the ```ndk-stack``` tool to symbolize your stacktrace. Note that you also need to know the architecture (armv7, arm64, x86, etc...) of the libraries that were used.
Here's how to get the stacktrace and the right architecture from a device plugged to your computer:
```
adb logcat -d | ndk-stack -sym ./libs-debug/`adb shell getprop ro.product.cpu.abi | tr -d '\r'`
```
## Create an APK with a different package name
@ -137,6 +164,6 @@ Due to the full app rewrite we can't re-use previous translations, so we'll be v
In order to submit a patch for inclusion in linphone's source code:
1. First make sure your patch applies to latest git sources before submitting: patches made to old versions can't and won't be merged.
2. Fill out and send us an email with the link of pullrequest and the [Contributor Agreement](https://linphone.org/sites/default/files/bc-contributor-agreement_0.pdf) for your patch to be included in the git tree.
2. Fill out and send us an email with the link of pull-request and the [Contributor Agreement](https://linphone.org/sites/default/files/bc-contributor-agreement_0.pdf) for your patch to be included in the git tree.
The goal of this agreement to grant us peaceful exercise of our rights on the linphone source code, while not losing your rights on your contribution.

View file

@ -5,6 +5,9 @@ plugins {
id 'org.jlleitschuh.gradle.ktlint'
}
def appVersionName = "4.6.0"
def appVersionCode = 40600 // 4.06.00
static def getPackageName() {
return "org.linphone"
}
@ -24,7 +27,7 @@ if (crashlyticsEnabled) {
def gitBranch = new ByteArrayOutputStream()
task getGitVersion() {
def gitVersion = "4.7.0"
def gitVersion = appVersionName
def gitVersionStream = new ByteArrayOutputStream()
def gitCommitsCount = new ByteArrayOutputStream()
def gitCommitHash = new ByteArrayOutputStream()
@ -52,9 +55,9 @@ task getGitVersion() {
} else {
gitVersion = gitVersionStream.toString().trim() + "." + gitCommitsCount.toString().trim() + "+" + gitCommitHash.toString().trim()
}
println("Git version: " + gitVersion)
println("Git version: " + gitVersion + " (" + appVersionCode + ")")
} catch (ignored) {
println("Git not found")
println("Git not found, using " + gitVersion + " (" + appVersionCode + ")")
}
project.version = gitVersion
}
@ -84,7 +87,7 @@ android {
defaultConfig {
minSdkVersion 23
targetSdkVersion 31
versionCode 4700
versionCode appVersionCode
versionName "${project.version}"
applicationId getPackageName()
}
@ -95,7 +98,7 @@ android {
}
// See https://developer.android.com/studio/releases/gradle-plugin#3-6-0-behavior for why extractNativeLibs is set to true in debug flavor
if (variant.buildType.name == "release" || variant.buildType.name == "releaseAppBundle") {
if (variant.buildType.name == "release" || variant.buildType.name == "releaseWithCrashlytics") {
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
linphone_file_provider: getPackageName() + ".fileprovider",
appLabel: "@string/app_name",
@ -143,8 +146,8 @@ android {
initWith release
resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString()
if (crashlyticsEnabled) {
if (crashlyticsEnabled) {
firebaseCrashlytics {
nativeSymbolUploadEnabled true
unstrippedNativeLibsDir file(LinphoneSdkBuildDir + '/libs-debug/').toString()
@ -161,14 +164,13 @@ android {
resValue "string", "sync_account_type", getPackageName() + ".sync"
resValue "string", "file_provider", getPackageName() + ".debug.fileprovider"
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"
resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString()
if (!firebaseEnabled) {
resValue "string", "gcm_defaultSenderId", "none"
}
resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString()
if (crashlyticsEnabled) {
firebaseCrashlytics {
nativeSymbolUploadEnabled true
unstrippedNativeLibsDir file(LinphoneSdkBuildDir + '/libs-debug/').toString()
@ -191,47 +193,28 @@ android {
}
}
repositories {
maven {
name "local linphone-sdk maven repository"
url file(LinphoneSdkBuildDir + '/maven_repository/')
content {
includeGroup "org.linphone"
}
}
maven {
name "linphone.org maven repository"
url "https://download.linphone.org/maven_repository"
content {
includeGroup "org.linphone"
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.media:media:1.4.3'
implementation 'androidx.fragment:fragment-ktx:1.4.0-beta01'
implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation 'androidx.core:core-ktx:1.7.0'
def nav_version = "2.4.0-beta01"
def nav_version = "2.4.0"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0-beta01"
implementation "androidx.window:window:1.0.0-beta03"
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
implementation "androidx.window:window:1.0.0"
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation "androidx.security:security-crypto-ktx:1.1.0-alpha03"
implementation 'androidx.core:core-splashscreen:1.0.0-beta01'
implementation 'com.google.android.material:material:1.4.0'
implementation 'com.google.android.material:material:1.5.0'
implementation 'com.google.android.flexbox:flexbox:3.0.0'
implementation 'androidx.emoji:emoji:1.1.0'

View file

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

View file

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

View file

@ -16,6 +16,7 @@
<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="push_notification_allowed" overwrite="true">1</entry>
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">1</entry>
</section>
<section name="nat_policy_default_values">
<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.util.DisplayMetrics
import android.view.Display
import android.view.Surface
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.lifecycleScope
import androidx.navigation.ActivityNavigator
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoRepository.Companion.windowInfoRepository
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
import java.util.*
import kotlinx.coroutines.Dispatchers
@ -58,9 +57,12 @@ abstract class GenericActivity : AppCompatActivity() {
ensureCoreExists(applicationContext)
lifecycleScope.launch(Dispatchers.Main) {
windowInfoRepository().windowLayoutInfo.collect { newLayoutInfo ->
updateCurrentLayout(newLayoutInfo)
}
WindowInfoTracker
.getOrCreate(this@GenericActivity)
.windowLayoutInfo(this@GenericActivity)
.collect { newLayoutInfo ->
updateCurrentLayout(newLayoutInfo)
}
}
requestedOrientation = if (corePreferences.forcePortrait) {
@ -97,18 +99,6 @@ abstract class GenericActivity : AppCompatActivity() {
override fun onResume() {
super.onResume()
var degrees = 270
val orientation = windowManager.defaultDisplay.rotation
when (orientation) {
Surface.ROTATION_0 -> degrees = 0
Surface.ROTATION_90 -> degrees = 270
Surface.ROTATION_180 -> degrees = 180
Surface.ROTATION_270 -> degrees = 90
}
Log.i("[Generic Activity] Device orientation is $degrees (raw value is $orientation)")
val rotation = (360 - degrees) % 360
coreContext.core.deviceRotation = rotation
// Remove service notification if it has been started by device boot
coreContext.notificationsManager.stopForegroundNotificationIfPossible()
}

View file

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

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 */
internal fun TabsFragment.navigateToCallHistory() {
@ -79,9 +103,8 @@ internal fun TabsFragment.navigateToCallHistory() {
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_masterCallLogsFragment
R.id.dialerFragment -> R.id.action_dialerFragment_to_masterCallLogsFragment
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_masterCallLogsFragment
else -> 0
else -> R.id.action_global_masterCallLogsFragment
}
if (action == 0) return
findNavController().navigate(
action,
null,
@ -94,9 +117,8 @@ internal fun TabsFragment.navigateToContacts() {
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_masterContactsFragment
R.id.dialerFragment -> R.id.action_dialerFragment_to_masterContactsFragment
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_masterContactsFragment
else -> 0
else -> R.id.action_global_masterContactsFragment
}
if (action == 0) return
findNavController().navigate(
action,
null,
@ -109,9 +131,8 @@ internal fun TabsFragment.navigateToDialer() {
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_dialerFragment
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_dialerFragment
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_dialerFragment
else -> 0
else -> R.id.action_global_dialerFragment
}
if (action == 0) return
findNavController().navigate(
action,
null,
@ -124,9 +145,8 @@ internal fun TabsFragment.navigateToChatRooms() {
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_masterChatRoomsFragment
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_masterChatRoomsFragment
R.id.dialerFragment -> R.id.action_dialerFragment_to_masterChatRoomsFragment
else -> 0
else -> R.id.action_global_masterChatRoomsFragment
}
if (action == 0) return
findNavController().navigate(
action,
null,
@ -298,7 +318,15 @@ internal fun DetailChatRoomFragment.navigateToEmptyChatRoom() {
findNavController().navigate(
R.id.action_global_emptyChatFragment,
null,
popupTo(R.id.emptyChatFragment, true)
popupTo(R.id.detailChatRoomFragment, true)
)
}
internal fun DetailChatRoomFragment.navigateToDialer(args: Bundle?) {
findMasterNavController().navigate(
R.id.action_global_dialerFragment,
args,
popupTo(R.id.dialerFragment, true)
)
}
@ -317,7 +345,7 @@ internal fun ChatRoomCreationFragment.navigateToChatRoom(args: Bundle) {
findNavController().navigate(
R.id.action_chatRoomCreationFragment_to_detailChatRoomFragment,
args,
popupTo(R.id.detailChatRoomFragment, true)
popupTo(R.id.chatRoomCreationFragment, true)
)
}
}
@ -326,7 +354,7 @@ internal fun ChatRoomCreationFragment.navigateToEmptyChatRoom() {
findNavController().navigate(
R.id.action_global_emptyChatFragment,
null,
popupTo(R.id.emptyChatFragment, true)
popupTo(R.id.chatRoomCreationFragment, true)
)
}
@ -782,10 +810,10 @@ internal fun WelcomeFragment.navigateToAccountLogin() {
}
}
internal fun WelcomeFragment.navigateToGenericLogin() {
internal fun WelcomeFragment.navigateToGenericLoginWarning() {
if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
findNavController().navigate(
R.id.action_welcomeFragment_to_genericAccountLoginFragment,
R.id.action_welcomeFragment_to_genericAccountWarningFragment,
null,
popupTo()
)
@ -822,6 +850,16 @@ internal fun AccountLoginFragment.navigateToPhoneAccountValidation(args: Bundle?
}
}
internal fun GenericAccountWarningFragment.navigateToGenericLogin() {
if (findNavController().currentDestination?.id == R.id.genericAccountWarningFragment) {
findNavController().navigate(
R.id.action_genericAccountWarningFragment_to_genericAccountLoginFragment,
null,
popupTo(R.id.welcomeFragment, popUpInclusive = false)
)
}
}
internal fun GenericAccountLoginFragment.navigateToEchoCancellerCalibration() {
if (findNavController().currentDestination?.id == R.id.genericAccountLoginFragment) {
findNavController().navigate(

View file

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

View file

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

View file

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

View file

@ -35,6 +35,7 @@ import org.linphone.activities.main.viewmodels.DialogViewModel
import org.linphone.activities.navigateToEchoCancellerCalibration
import org.linphone.activities.navigateToPhoneAccountValidation
import org.linphone.databinding.AssistantAccountLoginFragmentBinding
import org.linphone.mediastream.Version
import org.linphone.utils.DialogUtils
class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragmentBinding>() {
@ -52,7 +53,10 @@ class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragment
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
if (resources.getBoolean(R.bool.isTablet)) {
@ -75,66 +79,65 @@ class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragment
}
viewModel.goToSmsValidationEvent.observe(
viewLifecycleOwner,
{
it.consume {
val args = Bundle()
args.putBoolean("IsLogin", true)
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
navigateToPhoneAccountValidation(args)
}
viewLifecycleOwner
) {
it.consume {
val args = Bundle()
args.putBoolean("IsLogin", true)
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
navigateToPhoneAccountValidation(args)
}
)
}
viewModel.leaveAssistantEvent.observe(
viewLifecycleOwner,
{
it.consume {
coreContext.contactsManager.updateLocalContacts()
viewLifecycleOwner
) {
it.consume {
coreContext.contactsManager.updateLocalContacts()
if (coreContext.core.isEchoCancellerCalibrationRequired) {
navigateToEchoCancellerCalibration()
} else {
requireActivity().finish()
}
if (coreContext.core.isEchoCancellerCalibrationRequired) {
navigateToEchoCancellerCalibration()
} else {
requireActivity().finish()
}
}
)
}
viewModel.invalidCredentialsEvent.observe(
viewLifecycleOwner,
{
it.consume {
val dialogViewModel = DialogViewModel(getString(R.string.assistant_error_invalid_credentials))
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
viewLifecycleOwner
) {
it.consume {
val dialogViewModel =
DialogViewModel(getString(R.string.assistant_error_invalid_credentials))
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
dialogViewModel.showCancelButton {
viewModel.removeInvalidProxyConfig()
dialog.dismiss()
}
dialogViewModel.showDeleteButton(
{
viewModel.continueEvenIfInvalidCredentials()
dialog.dismiss()
},
getString(R.string.assistant_continue_even_if_credentials_invalid)
)
dialog.show()
dialogViewModel.showCancelButton {
viewModel.removeInvalidProxyConfig()
dialog.dismiss()
}
dialogViewModel.showDeleteButton(
{
viewModel.continueEvenIfInvalidCredentials()
dialog.dismiss()
},
getString(R.string.assistant_continue_even_if_credentials_invalid)
)
dialog.show()
}
)
}
viewModel.onErrorEvent.observe(
viewLifecycleOwner,
{
it.consume { message ->
(requireActivity() as AssistantActivity).showSnackBar(message)
}
viewLifecycleOwner
) {
it.consume { 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
class EchoCancellerCalibrationFragment : GenericFragment<AssistantEchoCancellerCalibrationFragmentBinding>() {
companion object {
const val RECORD_AUDIO_PERMISSION_REQUEST_CODE = 0
}
private lateinit var viewModel: EchoCancellerCalibrationViewModel
override fun getLayoutId(): Int = R.layout.assistant_echo_canceller_calibration_fragment
@ -44,17 +48,16 @@ class EchoCancellerCalibrationFragment : GenericFragment<AssistantEchoCancellerC
binding.viewModel = viewModel
viewModel.echoCalibrationTerminated.observe(
viewLifecycleOwner,
{
it.consume {
requireActivity().finish()
}
viewLifecycleOwner
) {
it.consume {
requireActivity().finish()
}
)
}
if (!PermissionHelper.required(requireContext()).hasRecordAudioPermission()) {
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 {
viewModel.startEchoCancellerCalibration()
}
@ -65,13 +68,16 @@ class EchoCancellerCalibrationFragment : GenericFragment<AssistantEchoCancellerC
permissions: Array<out String>,
grantResults: IntArray
) {
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
if (granted) {
Log.i("[Echo Canceller Calibration] RECORD_AUDIO permission granted")
viewModel.startEchoCancellerCalibration()
} else {
Log.w("[Echo Canceller Calibration] RECORD_AUDIO permission denied")
requireActivity().finish()
if (requestCode == RECORD_AUDIO_PERMISSION_REQUEST_CODE) {
val granted =
grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
if (granted) {
Log.i("[Echo Canceller Calibration] RECORD_AUDIO permission granted")
viewModel.startEchoCancellerCalibration()
} else {
Log.w("[Echo Canceller Calibration] RECORD_AUDIO permission denied")
requireActivity().finish()
}
}
}
}

View file

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

View file

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

View file

@ -54,52 +54,50 @@ class GenericAccountLoginFragment : GenericFragment<AssistantGenericAccountLogin
binding.viewModel = viewModel
viewModel.leaveAssistantEvent.observe(
viewLifecycleOwner,
{
it.consume {
coreContext.contactsManager.updateLocalContacts()
viewLifecycleOwner
) {
it.consume {
coreContext.contactsManager.updateLocalContacts()
if (coreContext.core.isEchoCancellerCalibrationRequired) {
navigateToEchoCancellerCalibration()
} else {
requireActivity().finish()
}
if (coreContext.core.isEchoCancellerCalibrationRequired) {
navigateToEchoCancellerCalibration()
} else {
requireActivity().finish()
}
}
)
}
viewModel.invalidCredentialsEvent.observe(
viewLifecycleOwner,
{
it.consume {
val dialogViewModel = DialogViewModel(getString(R.string.assistant_error_invalid_credentials))
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
viewLifecycleOwner
) {
it.consume {
val dialogViewModel =
DialogViewModel(getString(R.string.assistant_error_invalid_credentials))
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
dialogViewModel.showCancelButton {
viewModel.removeInvalidProxyConfig()
dialog.dismiss()
}
dialogViewModel.showDeleteButton(
{
viewModel.continueEvenIfInvalidCredentials()
dialog.dismiss()
},
getString(R.string.assistant_continue_even_if_credentials_invalid)
)
dialog.show()
dialogViewModel.showCancelButton {
viewModel.removeInvalidProxyConfig()
dialog.dismiss()
}
dialogViewModel.showDeleteButton(
{
viewModel.continueEvenIfInvalidCredentials()
dialog.dismiss()
},
getString(R.string.assistant_continue_even_if_credentials_invalid)
)
dialog.show()
}
)
}
viewModel.onErrorEvent.observe(
viewLifecycleOwner,
{
it.consume { message ->
(requireActivity() as AssistantActivity).showSnackBar(message)
}
viewLifecycleOwner
) {
it.consume { 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.navigateToPhoneAccountValidation
import org.linphone.databinding.AssistantPhoneAccountCreationFragmentBinding
import org.linphone.mediastream.Version
class PhoneAccountCreationFragment : AbstractPhoneFragment<AssistantPhoneAccountCreationFragmentBinding>() {
class PhoneAccountCreationFragment :
AbstractPhoneFragment<AssistantPhoneAccountCreationFragmentBinding>() {
private lateinit var sharedViewModel: SharedAssistantViewModel
override lateinit var viewModel: PhoneAccountCreationViewModel
@ -45,7 +47,10 @@ class PhoneAccountCreationFragment : AbstractPhoneFragment<AssistantPhoneAccount
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.setInfoClickListener {
@ -57,26 +62,26 @@ class PhoneAccountCreationFragment : AbstractPhoneFragment<AssistantPhoneAccount
}
viewModel.goToSmsValidationEvent.observe(
viewLifecycleOwner,
{
it.consume {
val args = Bundle()
args.putBoolean("IsCreation", true)
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
navigateToPhoneAccountValidation(args)
}
viewLifecycleOwner
) {
it.consume {
val args = Bundle()
args.putBoolean("IsCreation", true)
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
navigateToPhoneAccountValidation(args)
}
)
}
viewModel.onErrorEvent.observe(
viewLifecycleOwner,
{
it.consume { message ->
(requireActivity() as AssistantActivity).showSnackBar(message)
}
viewLifecycleOwner
) {
it.consume { 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.core.tools.Log
import org.linphone.databinding.AssistantPhoneAccountLinkingFragmentBinding
import org.linphone.mediastream.Version
class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountLinkingFragmentBinding>() {
private lateinit var sharedViewModel: SharedAssistantViewModel
@ -72,39 +73,38 @@ class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountL
}
viewModel.goToSmsValidationEvent.observe(
viewLifecycleOwner,
{
it.consume {
val args = Bundle()
args.putBoolean("IsLinking", true)
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
navigateToPhoneAccountValidation(args)
}
viewLifecycleOwner
) {
it.consume {
val args = Bundle()
args.putBoolean("IsLinking", true)
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
navigateToPhoneAccountValidation(args)
}
)
}
viewModel.leaveAssistantEvent.observe(
viewLifecycleOwner,
{
it.consume {
if (LinphoneApplication.coreContext.core.isEchoCancellerCalibrationRequired) {
navigateToEchoCancellerCalibration()
} else {
requireActivity().finish()
}
viewLifecycleOwner
) {
it.consume {
if (LinphoneApplication.coreContext.core.isEchoCancellerCalibrationRequired) {
navigateToEchoCancellerCalibration()
} else {
requireActivity().finish()
}
}
)
}
viewModel.onErrorEvent.observe(
viewLifecycleOwner,
{
it.consume { message ->
(requireActivity() as AssistantActivity).showSnackBar(message)
}
viewLifecycleOwner
) {
it.consume { 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.leaveAssistantEvent.observe(
viewLifecycleOwner,
{
it.consume {
when {
viewModel.isLogin.value == true || viewModel.isCreation.value == true -> {
coreContext.contactsManager.updateLocalContacts()
viewLifecycleOwner
) {
it.consume {
when {
viewModel.isLogin.value == true || viewModel.isCreation.value == true -> {
coreContext.contactsManager.updateLocalContacts()
if (coreContext.core.isEchoCancellerCalibrationRequired) {
navigateToEchoCancellerCalibration()
} else {
requireActivity().finish()
}
}
viewModel.isLinking.value == true -> {
val args = Bundle()
args.putString("Identity", "sip:${viewModel.accountCreator.username}@${viewModel.accountCreator.domain}")
navigateToAccountSettings(args)
if (coreContext.core.isEchoCancellerCalibrationRequired) {
navigateToEchoCancellerCalibration()
} else {
requireActivity().finish()
}
}
viewModel.isLinking.value == true -> {
val args = Bundle()
args.putString(
"Identity",
"sip:${viewModel.accountCreator.username}@${viewModel.accountCreator.domain}"
)
navigateToAccountSettings(args)
}
}
}
)
}
viewModel.onErrorEvent.observe(
viewLifecycleOwner,
{
it.consume { message ->
(requireActivity() as AssistantActivity).showSnackBar(message)
}
viewLifecycleOwner
) {
it.consume { message ->
(requireActivity() as AssistantActivity).showSnackBar(message)
}
)
}
val clipboard = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
clipboard.addPrimaryClipChangedListener {

View file

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

View file

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

View file

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

View file

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

View file

@ -25,6 +25,7 @@ import org.linphone.R
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.Event
import org.linphone.utils.PhoneNumberUtils
class AccountLoginViewModelFactory(private val accountCreator: AccountCreator) :
ViewModelProvider.NewInstanceFactory() {
@ -220,6 +221,18 @@ class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewM
proxyConfig.isPushNotificationAllowed = true
if (proxyConfig.dialPrefix.isNullOrEmpty()) {
val dialPlan = PhoneNumberUtils.getDialPlanForCurrentCountry(coreContext.context)
if (dialPlan != null) {
Log.i("[Assistant] [Account Login] Found dial plan country ${dialPlan.country} with international prefix ${dialPlan.countryCallingCode}")
proxyConfig.edit()
proxyConfig.dialPrefix = dialPlan.countryCallingCode
proxyConfig.done()
} else {
Log.w("[Assistant] [Account Login] Failed to find dial plan")
}
}
Log.i("[Assistant] [Account Login] Proxy config created")
return true
}

View file

@ -22,11 +22,13 @@ package org.linphone.activities.assistant.viewmodels
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import org.linphone.LinphoneApplication
import org.linphone.core.AccountCreator
import org.linphone.core.AccountCreatorListenerStub
import org.linphone.core.ProxyConfig
import org.linphone.core.tools.Log
import org.linphone.utils.Event
import org.linphone.utils.PhoneNumberUtils
class EmailAccountValidationViewModelFactory(private val accountCreator: AccountCreator) :
ViewModelProvider.NewInstanceFactory() {
@ -106,6 +108,18 @@ class EmailAccountValidationViewModel(val accountCreator: AccountCreator) : View
proxyConfig.isPushNotificationAllowed = true
if (proxyConfig.dialPrefix.isNullOrEmpty()) {
val dialPlan = PhoneNumberUtils.getDialPlanForCurrentCountry(LinphoneApplication.coreContext.context)
if (dialPlan != null) {
Log.i("[Assistant] [Account Validation] Found dial plan country ${dialPlan.country} with international prefix ${dialPlan.countryCallingCode}")
proxyConfig.edit()
proxyConfig.dialPrefix = dialPlan.countryCallingCode
proxyConfig.done()
} else {
Log.w("[Assistant] [Account Validation] Failed to find dial plan")
}
}
Log.i("[Assistant] [Account Validation] Proxy config created")
return true
}

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")
coreContext.core.removeListener(coreListener)
onErrorEvent.value = Event("Error: Failed to create account object")
waitForServerAnswer.value = false
return
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -60,10 +60,10 @@ class IncomingCallViewModel(call: Call) : CallViewModel(call) {
coreContext.core.addListener(listener)
screenLocked.value = false
inviteWithVideo.value = call.remoteParams?.videoEnabled() == true && coreContext.core.videoActivationPolicy.automaticallyAccept
inviteWithVideo.value = call.remoteParams?.isVideoEnabled == true && coreContext.core.videoActivationPolicy.automaticallyAccept
earlyMediaVideoEnabled.value = corePreferences.acceptEarlyMedia &&
call.state == Call.State.IncomingEarlyMedia &&
call.currentParams.videoEnabled()
call.currentParams.isVideoEnabled
}
override fun onCleared() {

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

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

View file

@ -58,5 +58,13 @@ class AboutFragment : SecureFragment<AboutFragmentBinding>() {
)
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.
private var loading = true
var userHasScrolledUp: Boolean = false
private var userHasScrolledUp: Boolean = false
// This happens many times a second during a scroll, so be wary of the code you place here.
// We are given a few useful parameters to help us work out if we need to load some more data,

View file

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

View file

@ -73,17 +73,16 @@ class ChatRoomsListAdapter(
// This is for item selection through ListTopBarFragment
selectionListViewModel = selectionViewModel
selectionViewModel.isEditionEnabled.observe(
viewLifecycleOwner,
{
position = adapterPosition
}
)
viewLifecycleOwner
) {
position = bindingAdapterPosition
}
forwardPending = isForwardPending
setClickListener {
if (selectionViewModel.isEditionEnabled.value == true) {
selectionViewModel.onToggleSelect(adapterPosition)
selectionViewModel.onToggleSelect(bindingAdapterPosition)
} else {
selectedChatRoomEvent.value = Event(chatRoomViewModel.chatRoom)
}
@ -116,6 +115,6 @@ private class ChatRoomDiffCallback : DiffUtil.ItemCallback<ChatRoomViewModel>()
oldItem: ChatRoomViewModel,
newItem: ChatRoomViewModel
): 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(
private val chatMessage: ChatMessage,
private val contentIndex: Int,
) {
var listener: OnContentClickedListener? = null
@ -60,7 +59,6 @@ class ChatMessageContentData(
val fileName = MutableLiveData<String>()
val filePath = MutableLiveData<String>()
val fileSize = MutableLiveData<String>()
val downloadable = MutableLiveData<Boolean>()
val downloadEnabled = MutableLiveData<Boolean>()
@ -72,13 +70,11 @@ class ChatMessageContentData(
val formattedDuration = MutableLiveData<String>()
val voiceRecordPlayingPosition = MutableLiveData<Int>()
val isVoiceRecordPlaying = MutableLiveData<Boolean>()
var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
val isAlone: Boolean
get() {
var count = 0
for (content in chatMessage.contents) {
val content = getContent()
if (content.isFileTransfer || content.isFile) {
count += 1
}
@ -86,7 +82,9 @@ class ChatMessageContentData(
return count == 1
}
var isFileEncrypted: Boolean = false
private var isFileEncrypted: Boolean = false
private var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
private lateinit var voiceRecordingPlayer: Player
private val playerListener = PlayerListener {
@ -145,13 +143,7 @@ class ChatMessageContentData(
fun destroy() {
scope.cancel()
val path = filePath.value.orEmpty()
if (path.isNotEmpty() && isFileEncrypted) {
Log.i("[Content] Deleting file used for preview: $path")
FileUtils.deleteFile(path)
filePath.value = ""
}
deletePlainFilePath()
chatMessage.removeListener(chatMessageListener)
if (this::voiceRecordingPlayer.isInitialized) {
@ -181,9 +173,22 @@ class ChatMessageContentData(
listener?.onContentClicked(getContent())
}
private fun deletePlainFilePath() {
val path = filePath.value.orEmpty()
if (path.isNotEmpty() && isFileEncrypted) {
Log.i("[Content] Deleting file used for preview: $path")
FileUtils.deleteFile(path)
filePath.value = ""
}
}
private fun updateContent() {
Log.i("[Content] Updating content")
deletePlainFilePath()
val content = getContent()
isFileEncrypted = content.isFileEncrypted
Log.i("[Content] Is ${if (content.isFile) "file" else "file transfer"} content encrypted ? $isFileEncrypted")
filePath.value = ""
fileName.value = if (content.name.isNullOrEmpty() && !content.filePath.isNullOrEmpty()) {
@ -193,14 +198,18 @@ class ChatMessageContentData(
}
// Display download size and underline text
fileSize.value = AppUtils.bytesToDisplayableSize(content.fileSize.toLong())
val spannable = SpannableString("${AppUtils.getString(R.string.chat_message_download_file)} (${fileSize.value})")
val fileSize = AppUtils.bytesToDisplayableSize(content.fileSize.toLong())
val spannable = SpannableString("${AppUtils.getString(R.string.chat_message_download_file)} ($fileSize)")
spannable.setSpan(UnderlineSpan(), 0, spannable.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
downloadLabel.value = spannable
if (content.isFile || (content.isFileTransfer && chatMessage.isOutgoing)) {
Log.i("[Content] Is content encrypted ? $isFileEncrypted")
val path = if (isFileEncrypted) content.plainFilePath else content.filePath ?: ""
val path = if (isFileEncrypted) {
Log.i("[Content] Content is encrypted, requesting plain file path")
content.plainFilePath
} else {
content.filePath ?: ""
}
downloadable.value = content.filePath.orEmpty().isEmpty()
if (path.isNotEmpty()) {
@ -226,7 +235,7 @@ class ChatMessageContentData(
}
}
} else {
Log.w("[Content] Found content with empty path...")
Log.w("[Content] Found ${if (content.isFile) "file" else "file transfer"} content with empty path...")
isImage.value = false
isVideo.value = false
isAudio.value = false
@ -297,8 +306,9 @@ class ChatMessageContentData(
private fun initVoiceRecordPlayer() {
Log.i("[Voice Recording] Creating player for voice record")
// Use speaker sound card to play recordings, otherwise use earpiece
// In case no headphones/headset is connected, use speaker sound card to play recordings, otherwise use earpiece
// If none are available, default one will be used
var headphonesCard: String? = null
var speakerCard: String? = null
var earpieceCard: String? = null
for (device in coreContext.core.audioDevices) {
@ -307,12 +317,14 @@ class ChatMessageContentData(
speakerCard = device.id
} else if (device.type == AudioDevice.Type.Earpiece) {
earpieceCard = device.id
} else if (device.type == AudioDevice.Type.Headphones || device.type == AudioDevice.Type.Headset) {
headphonesCard = device.id
}
}
}
Log.i("[Voice Recording] Found speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]")
Log.i("[Voice Recording] Found headset/headphones sound card [$headphonesCard], speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]")
val localPlayer = coreContext.core.createLocalPlayer(speakerCard ?: earpieceCard, null, null)
val localPlayer = coreContext.core.createLocalPlayer(headphonesCard ?: speakerCard ?: earpieceCard, null, null)
if (localPlayer != null) {
voiceRecordingPlayer = localPlayer
} else {
@ -321,8 +333,7 @@ class ChatMessageContentData(
}
voiceRecordingPlayer.addListener(playerListener)
val content = getContent()
val path = if (content.isFileEncrypted) content.plainFilePath else content.filePath ?: ""
val path = filePath.value
voiceRecordingPlayer.open(path.orEmpty())
voiceRecordDuration.value = voiceRecordingPlayer.duration
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(voiceRecordingPlayer.duration) // is already in milliseconds
@ -346,4 +357,6 @@ class ChatMessageContentData(
interface OnContentClickedListener {
fun onContentClicked(content: Content)
fun onSipAddressClicked(sipUri: String)
}

View file

@ -24,12 +24,14 @@ import android.text.Spannable
import android.text.util.Linkify
import androidx.core.text.util.LinkifyCompat
import androidx.lifecycle.MutableLiveData
import java.util.regex.Pattern
import org.linphone.R
import org.linphone.contact.GenericContactData
import org.linphone.core.ChatMessage
import org.linphone.core.ChatMessageListenerStub
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
import org.linphone.utils.PatternClickableSpan
import org.linphone.utils.TimestampUtils
class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMessage.fromAddress) {
@ -59,6 +61,9 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
val replyData = MutableLiveData<ChatMessageData>()
var hasPreviousMessage = false
var hasNextMessage = false
private var countDownTimer: CountDownTimer? = null
private val listener = object : ChatMessageListenerStub() {
@ -106,6 +111,11 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
}
fun updateBubbleBackground(hasPrevious: Boolean, hasNext: Boolean) {
hasPreviousMessage = hasPrevious
hasNextMessage = hasNext
hideTime.value = false
hideAvatar.value = false
if (hasPrevious) {
hideTime.value = true
}
@ -165,16 +175,25 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
val list = arrayListOf<ChatMessageContentData>()
val contentsList = chatMessage.contents
for (index in 0 until contentsList.size) {
for (index in contentsList.indices) {
val content = contentsList[index]
if (content.isFileTransfer || content.isFile) {
val data = ChatMessageContentData(chatMessage, index)
data.listener = contentListener
list.add(data)
} else if (content.isText) {
val spannable = Spannable.Factory.getInstance().newSpannable(content.utf8Text)
val spannable = Spannable.Factory.getInstance().newSpannable(content.utf8Text?.trim())
LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS)
text.value = spannable
text.value = PatternClickableSpan()
.add(
Pattern.compile("(sips?):([^@]+)(?:@([^ ]+))?"),
object : PatternClickableSpan.SpannableClickedListener {
override fun onSpanClicked(text: String) {
Log.i("[Chat Message Data] Clicked on SIP URI: $text")
contentListener?.onSipAddressClicked(text)
}
}
).build(spannable)
}
}

View file

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

View file

@ -29,19 +29,20 @@ import android.provider.MediaStore
import android.view.*
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.PopupWindow
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.view.doOnPreDraw
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import java.io.File
import java.lang.IllegalArgumentException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.*
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
@ -52,6 +53,7 @@ import org.linphone.activities.main.chat.adapters.ChatMessagesListAdapter
import org.linphone.activities.main.chat.data.ChatMessageData
import org.linphone.activities.main.chat.data.EventLogData
import org.linphone.activities.main.chat.viewmodels.*
import org.linphone.activities.main.chat.views.RichEditTextSendListener
import org.linphone.activities.main.fragments.MasterFragment
import org.linphone.activities.main.viewmodels.DialogViewModel
import org.linphone.activities.main.viewmodels.SharedMainViewModel
@ -107,6 +109,20 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
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?) {
super.onViewCreated(view, savedInstanceState)
@ -119,20 +135,18 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false
view.doOnPreDraw {
// Notifies fragment is ready to be drawn
sharedViewModel.chatRoomFragmentOpenedEvent.value = Event(true)
}
val localSipUri = arguments?.getString("LocalSipUri")
val remoteSipUri = arguments?.getString("RemoteSipUri")
val localSipUri = arguments?.getString("LocalSipUri") ?: savedInstanceState?.getString("LocalSipUri")
val remoteSipUri = arguments?.getString("RemoteSipUri") ?: savedInstanceState?.getString("RemoteSipUri")
val textToShare = arguments?.getString("TextToShare")
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()
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 remoteSipAddress = Factory.instance().createAddress(remoteSipUri)
@ -152,14 +166,29 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
return
}
view.doOnPreDraw {
// Notifies fragment is ready to be drawn
sharedViewModel.chatRoomFragmentOpenedEvent.value = Event(true)
}
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,
ChatRoomViewModelFactory(chatRoom)
)[ChatRoomViewModel::class.java]
binding.viewModel = viewModel
chatSendingViewModel = ViewModelProvider(
@ -193,24 +222,38 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
.addOnGlobalLayoutListener(
object : OnGlobalLayoutListener {
override fun onGlobalLayout() {
binding.chatMessagesList
.viewTreeObserver
.removeOnGlobalLayoutListener(this)
Log.i("[Chat Room] Messages have been displayed, scrolling to first unread message if any")
scrollToFirstUnreadMessageOrBottom(false)
if (isBindingAvailable()) {
binding.chatMessagesList
.viewTreeObserver
.removeOnGlobalLayoutListener(this)
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
/*val swipeConfiguration = RecyclerViewSwipeConfiguration()
swipeConfiguration.leftToRightAction = RecyclerViewSwipeConfiguration.Action(icon = R.drawable.menu_reply_default)
val swipeConfiguration = RecyclerViewSwipeConfiguration()
// 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 {
override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) {
adapter.notifyItemChanged(viewHolder.adapterPosition)
adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
val chatMessageEventLog = adapter.currentList[viewHolder.adapterPosition]
val chatMessage = chatMessageEventLog.chatMessage
val chatMessageEventLog = adapter.currentList[viewHolder.bindingAdapterPosition]
val chatMessage = chatMessageEventLog.eventLog.chatMessage
if (chatMessage != null) {
chatSendingViewModel.pendingChatMessageToReplyTo.value?.destroy()
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)
.attachToRecyclerView(binding.chatMessagesList)*/
RecyclerViewSwipeUtils(ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT, swipeConfiguration, swipeListener)
.attachToRecyclerView(binding.chatMessagesList)
val chatScrollListener = object : ChatScrollListener(layoutManager) {
override fun onLoadMore(totalItemsCount: Int) {
@ -236,7 +285,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
override fun onScrolledToEnd() {
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")
viewModel.chatRoom.markAsRead()
}
@ -245,192 +294,204 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
binding.chatMessagesList.addOnScrollListener(chatScrollListener)
chatSendingViewModel.textToSend.observe(
viewLifecycleOwner,
{
chatSendingViewModel.onTextToSendChanged(it)
}
)
viewLifecycleOwner
) {
chatSendingViewModel.onTextToSendChanged(it)
}
chatSendingViewModel.requestRecordAudioPermissionEvent.observe(
viewLifecycleOwner,
{
it.consume {
Log.i("[Chat Room] Asking for RECORD_AUDIO permission")
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 2)
}
viewLifecycleOwner
) {
it.consume {
Log.i("[Chat Room] Asking for RECORD_AUDIO permission")
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 2)
}
)
}
listViewModel.events.observe(
viewLifecycleOwner,
{ events ->
adapter.setUnreadMessageCount(viewModel.chatRoom.unreadMessagesCount, viewModel.isUserScrollingUp.value == true)
adapter.submitList(events)
}
)
viewLifecycleOwner
) { events ->
adapter.setUnreadMessageCount(
viewModel.chatRoom.unreadMessagesCount,
viewModel.isUserScrollingUp.value == true
)
adapter.submitList(events)
}
listViewModel.messageUpdatedEvent.observe(
viewLifecycleOwner,
{
it.consume { position ->
adapter.notifyItemChanged(position)
}
viewLifecycleOwner
) {
it.consume { position ->
adapter.notifyItemChanged(position)
}
)
}
listViewModel.requestWriteExternalStoragePermissionEvent.observe(
viewLifecycleOwner,
{
it.consume {
requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
}
viewLifecycleOwner
) {
it.consume {
requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
}
)
}
adapter.deleteMessageEvent.observe(
viewLifecycleOwner,
{
it.consume { chatMessage ->
listViewModel.deleteMessage(chatMessage)
}
viewLifecycleOwner
) {
it.consume { chatMessage ->
listViewModel.deleteMessage(chatMessage)
viewModel.updateLastMessageToDisplay()
}
)
}
adapter.resendMessageEvent.observe(
viewLifecycleOwner,
{
it.consume { chatMessage ->
listViewModel.resendMessage(chatMessage)
}
viewLifecycleOwner
) {
it.consume { chatMessage ->
listViewModel.resendMessage(chatMessage)
}
)
}
adapter.forwardMessageEvent.observe(
viewLifecycleOwner,
{
it.consume { chatMessage ->
// Remove observer before setting the message to forward
// as we don't want to forward it in this chat room
sharedViewModel.messageToForwardEvent.removeObservers(viewLifecycleOwner)
sharedViewModel.messageToForwardEvent.value = Event(chatMessage)
sharedViewModel.isPendingMessageForward.value = true
viewLifecycleOwner
) {
it.consume { chatMessage ->
// Remove observer before setting the message to forward
// as we don't want to forward it in this chat room
sharedViewModel.messageToForwardEvent.removeObservers(viewLifecycleOwner)
sharedViewModel.messageToForwardEvent.value = Event(chatMessage)
sharedViewModel.isPendingMessageForward.value = true
if (sharedViewModel.isSlidingPaneSlideable.value == true) {
Log.i("[Chat Room] Forwarding message, going to chat rooms list")
sharedViewModel.closeSlidingPaneEvent.value = Event(true)
} else {
navigateToEmptyChatRoom()
}
if (sharedViewModel.isSlidingPaneSlideable.value == true) {
Log.i("[Chat Room] Forwarding message, going to chat rooms list")
sharedViewModel.closeSlidingPaneEvent.value = Event(true)
} else {
navigateToEmptyChatRoom()
}
}
)
}
adapter.replyMessageEvent.observe(
viewLifecycleOwner,
{
it.consume { chatMessage ->
chatSendingViewModel.pendingChatMessageToReplyTo.value?.destroy()
chatSendingViewModel.pendingChatMessageToReplyTo.value = ChatMessageData(chatMessage)
chatSendingViewModel.isPendingAnswer.value = true
}
viewLifecycleOwner
) {
it.consume { chatMessage ->
chatSendingViewModel.pendingChatMessageToReplyTo.value?.destroy()
chatSendingViewModel.pendingChatMessageToReplyTo.value =
ChatMessageData(chatMessage)
chatSendingViewModel.isPendingAnswer.value = true
}
)
}
adapter.showImdnForMessageEvent.observe(
viewLifecycleOwner,
{
it.consume { chatMessage ->
val args = Bundle()
args.putString("MessageId", chatMessage.messageId)
navigateToImdn(args)
}
viewLifecycleOwner
) {
it.consume { chatMessage ->
val args = Bundle()
args.putString("MessageId", chatMessage.messageId)
navigateToImdn(args)
}
)
}
adapter.addSipUriToContactEvent.observe(
viewLifecycleOwner,
{
it.consume { sipUri ->
Log.i("[Chat Room] Going to contacts list with SIP URI to add: $sipUri")
sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event(R.id.masterChatRoomsFragment)
navigateToContacts(sipUri)
}
viewLifecycleOwner
) {
it.consume { sipUri ->
Log.i("[Chat Room] Going to contacts list with SIP URI to add: $sipUri")
sharedViewModel.updateContactsAnimationsBasedOnDestination.value =
Event(R.id.masterChatRoomsFragment)
navigateToContacts(sipUri)
}
)
}
adapter.openContentEvent.observe(
viewLifecycleOwner,
{
it.consume { content ->
val path = content.filePath.orEmpty()
viewLifecycleOwner
) {
it.consume { content ->
val path = content.filePath.orEmpty()
if (!File(path).exists()) {
(requireActivity() as MainActivity).showSnackBar(R.string.chat_room_file_not_found)
} else {
Log.i("[Chat Message] Opening file: $path")
sharedViewModel.contentToOpen.value = content
if (!File(path).exists()) {
(requireActivity() as MainActivity).showSnackBar(R.string.chat_room_file_not_found)
} else {
Log.i("[Chat Message] Opening file: $path")
sharedViewModel.contentToOpen.value = content
if (corePreferences.useInAppFileViewerForNonEncryptedFiles || content.isFileEncrypted) {
val preventScreenshots =
viewModel.chatRoom.currentParams.encryptionEnabled()
when {
FileUtils.isExtensionImage(path) -> navigateToImageFileViewer(
preventScreenshots
)
FileUtils.isExtensionVideo(path) -> navigateToVideoFileViewer(
preventScreenshots
)
FileUtils.isExtensionAudio(path) -> navigateToAudioFileViewer(
preventScreenshots
)
FileUtils.isExtensionPdf(path) -> navigateToPdfFileViewer(
preventScreenshots
)
FileUtils.isPlainTextFile(path) -> navigateToTextFileViewer(
preventScreenshots
)
else -> {
if (content.isFileEncrypted) {
Log.w("[Chat Message] File is encrypted and can't be opened in one of our viewers...")
showDialogForUserConsentBeforeExportingFileInThirdPartyApp(content)
} else if (!FileUtils.openFileInThirdPartyApp(requireActivity(), path)) {
showDialogToSuggestOpeningFileAsText()
}
if (corePreferences.useInAppFileViewerForNonEncryptedFiles || content.isFileEncrypted) {
val preventScreenshots =
viewModel.chatRoom.currentParams.isEncryptionEnabled
when {
FileUtils.isExtensionImage(path) -> navigateToImageFileViewer(
preventScreenshots
)
FileUtils.isExtensionVideo(path) -> navigateToVideoFileViewer(
preventScreenshots
)
FileUtils.isExtensionAudio(path) -> navigateToAudioFileViewer(
preventScreenshots
)
FileUtils.isExtensionPdf(path) -> navigateToPdfFileViewer(
preventScreenshots
)
FileUtils.isPlainTextFile(path) -> navigateToTextFileViewer(
preventScreenshots
)
else -> {
if (content.isFileEncrypted) {
Log.w("[Chat Message] File is encrypted and can't be opened in one of our viewers...")
showDialogForUserConsentBeforeExportingFileInThirdPartyApp(
content
)
} else if (!FileUtils.openFileInThirdPartyApp(
requireActivity(),
path
)
) {
showDialogToSuggestOpeningFileAsText()
}
}
} else {
if (!FileUtils.openFileInThirdPartyApp(requireActivity(), path)) {
showDialogToSuggestOpeningFileAsText()
}
}
} else {
if (!FileUtils.openFileInThirdPartyApp(requireActivity(), path)) {
showDialogToSuggestOpeningFileAsText()
}
}
}
}
)
}
adapter.sipUriClickedEvent.observe(
viewLifecycleOwner
) {
it.consume { sipUri ->
val args = Bundle()
args.putString("URI", sipUri)
args.putBoolean("Transfer", false)
// If auto start call setting is enabled, ignore it
args.putBoolean("SkipAutoCallStart", true)
navigateToDialer(args)
}
}
adapter.scrollToChatMessageEvent.observe(
viewLifecycleOwner,
{
it.consume { chatMessage ->
val events = listViewModel.events.value.orEmpty()
val eventLog = events.find { eventLog ->
if (eventLog.eventLog.type == EventLog.Type.ConferenceChatMessage) {
(eventLog.data as ChatMessageData).chatMessage.messageId == chatMessage.messageId
} else false
}
val index = events.indexOf(eventLog)
try {
if (corePreferences.enableAnimations) {
binding.chatMessagesList.smoothScrollToPosition(index)
} else {
binding.chatMessagesList.scrollToPosition(index)
}
} catch (iae: IllegalArgumentException) {
Log.e("[Chat Room] Can't scroll to position $index")
viewLifecycleOwner
) {
it.consume { chatMessage ->
val events = listViewModel.events.value.orEmpty()
val eventLog = events.find { eventLog ->
if (eventLog.eventLog.type == EventLog.Type.ConferenceChatMessage) {
(eventLog.data as ChatMessageData).chatMessage.messageId == chatMessage.messageId
} else false
}
val index = events.indexOf(eventLog)
try {
if (corePreferences.enableAnimations) {
binding.chatMessagesList.smoothScrollToPosition(index)
} else {
binding.chatMessagesList.scrollToPosition(index)
}
} catch (iae: IllegalArgumentException) {
Log.e("[Chat Room] Can't scroll to position $index")
}
}
)
}
binding.setBackClickListener {
goBack()
@ -493,8 +554,20 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
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 {
scrollToFirstUnreadMessageOrBottom(true)
viewModel.isUserScrollingUp.value = false
}
if (textToShare?.isNotEmpty() == true) {
@ -509,33 +582,43 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
}
sharedViewModel.richContentUri.observe(
viewLifecycleOwner,
{
it.consume { uri ->
Log.i("[Chat] Found rich content URI: $uri")
lifecycleScope.launch {
withContext(Dispatchers.Main) {
val path = FileUtils.getFilePath(requireContext(), uri)
Log.i("[Chat] Rich content URI: $uri matching path is: $path")
if (path != null) {
chatSendingViewModel.addAttachment(path)
}
viewLifecycleOwner
) {
it.consume { uri ->
Log.i("[Chat] Found rich content URI: $uri")
lifecycleScope.launch {
withContext(Dispatchers.Main) {
val path = FileUtils.getFilePath(requireContext(), uri)
Log.i("[Chat] Rich content URI: $uri matching path is: $path")
if (path != null) {
chatSendingViewModel.addAttachment(path)
}
}
}
}
)
}
sharedViewModel.messageToForwardEvent.observe(
viewLifecycleOwner,
{
it.consume { chatMessage ->
Log.i("[Chat Room] Found message to transfer")
showForwardConfirmationDialog(chatMessage)
sharedViewModel.isPendingMessageForward.value = false
}
viewLifecycleOwner
) {
it.consume { chatMessage ->
Log.i("[Chat Room] Found message to transfer")
showForwardConfirmationDialog(chatMessage)
sharedViewModel.isPendingMessageForward.value = false
}
)
}
binding.stubbedMessageToReplyTo.setOnInflateListener { _, inflated ->
Log.i("[Chat Room] Replying to message layout inflated")
val binding = DataBindingUtil.bind<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>) {
@ -545,6 +628,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
list.add(eventLog)
}
listViewModel.deleteEventLogs(list)
viewModel.updateLastMessageToDisplay()
}
override fun onRequestPermissionsResult(
@ -607,6 +691,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
}
override fun goBack() {
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
if (!findNavController().popBackStack()) {
if (sharedViewModel.isSlidingPaneSlideable.value == true) {
if (_adapter != null) {
@ -772,6 +857,35 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
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) {
if (_adapter != null && adapter.itemCount > 0) {
// Scroll to first unread message if any
@ -858,11 +972,13 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
{
dialog.dismiss()
lifecycleScope.launch {
Log.w("[Chat Room] Content is encrypted, requesting plain file path")
val plainFilePath = content.plainFilePath
Log.i("[Cht Room] Making a copy of [$plainFilePath] to the cache directory before exporting it")
val cacheCopyPath = FileUtils.copyFileToCache(plainFilePath)
if (cacheCopyPath != null) {
Log.i("[Cht Room] Cache copy has been made: $cacheCopyPath")
FileUtils.deleteFile(plainFilePath)
if (!FileUtils.openFileInThirdPartyApp(requireActivity(), cacheCopyPath)) {
showDialogToSuggestOpeningFileAsText()
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -142,9 +142,7 @@ class ChatRoomsListViewModel : ErrorReportingViewModel() {
}
private fun updateChatRooms() {
for (chatRoomViewModel in chatRooms.value.orEmpty()) {
chatRoomViewModel.destroy()
}
chatRooms.value.orEmpty().forEach(ChatRoomViewModel::destroy)
val list = arrayListOf<ChatRoomViewModel>()
for (chatRoom in coreContext.core.chatRooms) {
@ -155,6 +153,14 @@ class ChatRoomsListViewModel : ErrorReportingViewModel() {
}
private fun addChatRoom(chatRoom: ChatRoom) {
val exists = chatRooms.value.orEmpty().find {
it.chatRoom.localAddress.weakEqual(chatRoom.localAddress) && it.chatRoom.peerAddress.weakEqual(chatRoom.peerAddress)
}
if (exists != null) {
Log.w("[Chat Rooms] Do not add chat room to list, it's already here")
return
}
val list = arrayListOf<ChatRoomViewModel>()
val viewModel = ChatRoomViewModel(chatRoom)
list.add(viewModel)
@ -170,12 +176,10 @@ class ChatRoomsListViewModel : ErrorReportingViewModel() {
}
private fun findChatRoomIndex(chatRoom: ChatRoom): Int {
var index = 0
for (chatRoomViewModel in chatRooms.value.orEmpty()) {
for ((index, chatRoomViewModel) in chatRooms.value.orEmpty().withIndex()) {
if (chatRoomViewModel.chatRoom == chatRoom) {
return index
}
index++
}
return -1
}

View file

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

View file

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

View file

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

View file

@ -21,6 +21,7 @@ package org.linphone.activities.main.chat.views
import android.content.Context
import android.text.Layout
import android.text.method.LinkMovementMethod
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import kotlin.math.ceil
@ -40,6 +41,12 @@ class MultiLineWrapContentWidthTextView : AppCompatTextView {
defStyleAttr: Int
) : super(context, attrs, defStyleAttr)
override fun setText(text: CharSequence?, type: BufferType?) {
super.setText(text, type)
// Required for PatternClickableSpan
movementMethod = LinkMovementMethod.getInstance()
}
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
var wSpec = widthSpec
val widthMode = MeasureSpec.getMode(wSpec)

View file

@ -22,6 +22,7 @@ package org.linphone.activities.main.chat.views
import android.app.Activity
import android.content.Context
import android.util.AttributeSet
import android.view.KeyEvent
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.view.ViewCompat
import androidx.lifecycle.ViewModelProvider
@ -35,6 +36,10 @@ import org.linphone.utils.Event
* Allows for image input inside an EditText, usefull for keyboards with gif support for example.
*/
class RichEditText : AppCompatEditText {
private var controlPressed = false
private var sendListener: RichEditTextSendListener? = null
constructor(context: Context) : super(context) {
initReceiveContentListener()
}
@ -51,6 +56,10 @@ class RichEditText : AppCompatEditText {
initReceiveContentListener()
}
fun setControlEnterListener(listener: RichEditTextSendListener) {
sendListener = listener
}
private fun initReceiveContentListener() {
ViewCompat.setOnReceiveContentListener(
this, RichContentReceiver.MIME_TYPES,
@ -63,5 +72,25 @@ class RichEditText : AppCompatEditText {
sharedViewModel.richContentUri.value = Event(uri)
}
)
setOnKeyListener { _, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_CTRL_LEFT) {
if (event.action == KeyEvent.ACTION_DOWN) {
controlPressed = true
} else if (event.action == KeyEvent.ACTION_UP) {
controlPressed = false
}
false
} else if (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_UP && controlPressed) {
sendListener?.onControlEnterPressedAndReleased()
true
} else {
false
}
}
}
}
interface RichEditTextSendListener {
fun onControlEnterPressedAndReleased()
}

View file

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

View file

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

View file

@ -85,75 +85,59 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
useMaterialSharedAxisXForwardAnimation = false
sharedViewModel.updateContactsAnimationsBasedOnDestination.observe(
viewLifecycleOwner,
{
it.consume { id ->
val forward = when (id) {
R.id.dialerFragment, R.id.masterChatRoomsFragment -> false
else -> true
}
if (corePreferences.enableAnimations) {
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
val axis = if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y
enterTransition = MaterialSharedAxis(axis, forward)
reenterTransition = MaterialSharedAxis(axis, forward)
returnTransition = MaterialSharedAxis(axis, !forward)
exitTransition = MaterialSharedAxis(axis, !forward)
}
viewLifecycleOwner
) {
it.consume { id ->
val forward = when (id) {
R.id.dialerFragment, R.id.masterChatRoomsFragment -> false
else -> true
}
if (corePreferences.enableAnimations) {
val portraitOrientation =
resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
val axis =
if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y
enterTransition = MaterialSharedAxis(axis, forward)
reenterTransition = MaterialSharedAxis(axis, forward)
returnTransition = MaterialSharedAxis(axis, !forward)
exitTransition = MaterialSharedAxis(axis, !forward)
}
}
)
}
sharedViewModel.contactFragmentOpenedEvent.observe(
viewLifecycleOwner,
{
it.consume {
binding.slidingPane.openPane()
}
viewLifecycleOwner
) {
it.consume {
binding.slidingPane.openPane()
}
)
}
sharedViewModel.closeSlidingPaneEvent.observe(
viewLifecycleOwner,
{
it.consume {
if (!binding.slidingPane.closePane()) {
goBack()
}
viewLifecycleOwner
) {
it.consume {
if (!binding.slidingPane.closePane()) {
goBack()
}
}
)
}
sharedViewModel.layoutChangedEvent.observe(
viewLifecycleOwner,
{
it.consume {
sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
if (binding.slidingPane.isSlideable) {
val navHostFragment = childFragmentManager.findFragmentById(R.id.contacts_nav_container) as NavHostFragment
if (navHostFragment.navController.currentDestination?.id == R.id.emptyContactFragment) {
Log.i("[Contacts] Foldable device has been folded, closing side pane with empty fragment")
binding.slidingPane.closePane()
}
viewLifecycleOwner
) {
it.consume {
sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
if (binding.slidingPane.isSlideable) {
val navHostFragment =
childFragmentManager.findFragmentById(R.id.contacts_nav_container) as NavHostFragment
if (navHostFragment.navController.currentDestination?.id == R.id.emptyContactFragment) {
Log.i("[Contacts] Foldable device has been folded, closing side pane with empty fragment")
binding.slidingPane.closePane()
}
}
}
)
}
binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
/*binding.slidingPane.addPanelSlideListener(object : SlidingPaneLayout.PanelSlideListener {
override fun onPanelSlide(panel: View, slideOffset: Float) { }
override fun onPanelOpened(panel: View) {
if (binding.slidingPane.isSlideable) {
(requireActivity() as MainActivity).hideTabsFragment()
}
}
override fun onPanelClosed(panel: View) {
if (binding.slidingPane.isSlideable) {
(requireActivity() as MainActivity).showTabsFragment()
}
}
})*/
/* End of shared view model & sliding pane related */
@ -190,13 +174,13 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
viewModel.showCancelButton {
adapter.notifyItemChanged(viewHolder.adapterPosition)
adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
dialog.dismiss()
}
viewModel.showDeleteButton(
{
val deletedContact = adapter.currentList[viewHolder.adapterPosition].contactInternal
val deletedContact = adapter.currentList[viewHolder.bindingAdapterPosition].contactInternal
listViewModel.deleteContact(deletedContact)
if (!binding.slidingPane.isSlideable &&
deletedContact == sharedViewModel.selectedContact.value
@ -223,38 +207,36 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
binding.contactsList.addItemDecoration(headerItemDecoration)
adapter.selectedContactEvent.observe(
viewLifecycleOwner,
{
it.consume { contact ->
Log.i("[Contacts] Selected item in list changed: $contact")
sharedViewModel.selectedContact.value = contact
viewLifecycleOwner
) {
it.consume { contact ->
Log.i("[Contacts] Selected item in list changed: $contact")
sharedViewModel.selectedContact.value = contact
if (editOnClick) {
navigateToContactEditor(sipUriToAdd, binding.slidingPane)
editOnClick = false
sipUriToAdd = null
} else {
navigateToContact()
}
if (editOnClick) {
navigateToContactEditor(sipUriToAdd, binding.slidingPane)
editOnClick = false
sipUriToAdd = null
} else {
navigateToContact()
}
}
)
}
listViewModel.contactsList.observe(
viewLifecycleOwner,
{
val id = contactIdToDisplay
if (id != null) {
val contact = coreContext.contactsManager.findContactById(id)
if (contact != null) {
contactIdToDisplay = null
Log.i("[Contacts] Found matching contact $contact after callback")
adapter.selectedContactEvent.value = Event(contact)
}
viewLifecycleOwner
) {
val id = contactIdToDisplay
if (id != null) {
val contact = coreContext.contactsManager.findContactById(id)
if (contact != null) {
contactIdToDisplay = null
Log.i("[Contacts] Found matching contact $contact after callback")
adapter.selectedContactEvent.value = Event(contact)
}
adapter.submitList(it)
}
)
adapter.submitList(it)
}
binding.setAllContactsToggleClickListener {
listViewModel.sipContactsSelected.value = false
@ -264,18 +246,16 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
}
listViewModel.sipContactsSelected.observe(
viewLifecycleOwner,
{
listViewModel.updateContactsList()
}
)
viewLifecycleOwner
) {
listViewModel.updateContactsList()
}
listViewModel.filter.observe(
viewLifecycleOwner,
{
listViewModel.updateContactsList()
}
)
viewLifecycleOwner
) {
listViewModel.updateContactsList()
}
binding.setNewContactClickListener {
// 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.navigateToConfigFileViewer
import org.linphone.activities.navigateToContacts
import org.linphone.compatibility.Compatibility
import org.linphone.core.tools.Log
import org.linphone.databinding.DialerFragmentBinding
import org.linphone.mediastream.Version
import org.linphone.telecom.TelecomHelper
import org.linphone.utils.AppUtils
import org.linphone.utils.DialogUtils
import org.linphone.utils.Event
@ -75,24 +77,25 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
useMaterialSharedAxisXForwardAnimation = false
sharedViewModel.updateDialerAnimationsBasedOnDestination.observe(
viewLifecycleOwner,
{
it.consume { id ->
val forward = when (id) {
R.id.masterChatRoomsFragment -> false
else -> true
}
if (corePreferences.enableAnimations) {
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
val axis = if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y
enterTransition = MaterialSharedAxis(axis, forward)
reenterTransition = MaterialSharedAxis(axis, forward)
returnTransition = MaterialSharedAxis(axis, !forward)
exitTransition = MaterialSharedAxis(axis, !forward)
}
viewLifecycleOwner
) {
it.consume { id ->
val forward = when (id) {
R.id.masterChatRoomsFragment -> false
else -> true
}
if (corePreferences.enableAnimations) {
val portraitOrientation =
resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
val axis =
if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y
enterTransition = MaterialSharedAxis(axis, forward)
reenterTransition = MaterialSharedAxis(axis, forward)
returnTransition = MaterialSharedAxis(axis, !forward)
exitTransition = MaterialSharedAxis(axis, !forward)
}
}
)
}
binding.setNewContactClickListener {
sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.masterContactsFragment)
@ -108,6 +111,47 @@ class DialerFragment : SecureFragment<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) {
sharedViewModel.pendingCallTransfer = arguments?.getBoolean("Transfer") ?: false
Log.i("[Dialer] Is pending call transfer: ${sharedViewModel.pendingCallTransfer}")
@ -127,45 +171,6 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
}
arguments?.clear()
viewModel.enteredUri.observe(
viewLifecycleOwner,
{
if (it == corePreferences.debugPopupCode) {
displayDebugPopup()
viewModel.enteredUri.value = ""
}
}
)
viewModel.uploadFinishedEvent.observe(
viewLifecycleOwner,
{
it.consume { url ->
// To prevent being trigger when using the Send Logs button in About page
if (uploadLogsInitiatedByUs) {
val clipboard =
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Logs url", url)
clipboard.setPrimaryClip(clip)
val activity = requireActivity() as MainActivity
activity.showSnackBar(R.string.logs_url_copied_to_clipboard)
AppUtils.shareUploadedLogsUrl(activity, url)
}
}
}
)
viewModel.updateAvailableEvent.observe(
viewLifecycleOwner,
{
it.consume { url ->
displayNewVersionAvailableDialog(url)
}
}
)
Log.i("[Dialer] Pending call transfer mode = ${sharedViewModel.pendingCallTransfer}")
viewModel.transferVisibility.value = sharedViewModel.pendingCallTransfer
@ -205,18 +210,72 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
Log.i("[Dialer] READ_PHONE_STATE permission has been granted")
coreContext.initPhoneStateListener()
}
checkTelecomManagerPermissions()
} else if (requestCode == 1) {
var allGranted = true
for (result in grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
allGranted = false
}
}
if (allGranted) {
Log.i("[Dialer] Telecom Manager permission have been granted")
enableTelecomManager()
} else {
Log.w("[Dialer] Telecom Manager permission have been denied (at least one of them)")
}
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
@TargetApi(Version.API23_MARSHMALLOW_60)
private fun checkPermissions() {
checkReadPhoneStatePermission()
if (Version.sdkAboveOrEqual(Version.API26_O_80) && PermissionHelper.get().hasReadPhoneStatePermission()) {
// Don't check the following the previous permission is being asked
checkTelecomManagerPermissions()
}
}
@TargetApi(Version.API23_MARSHMALLOW_60)
private fun checkReadPhoneStatePermission() {
if (!PermissionHelper.get().hasReadPhoneStatePermission()) {
Log.i("[Dialer] Asking for READ_PHONE_STATE permission")
requestPermissions(arrayOf(Manifest.permission.READ_PHONE_STATE), 0)
}
}
@TargetApi(Version.API26_O_80)
private fun checkTelecomManagerPermissions() {
if (!corePreferences.useTelecomManager) {
Log.i("[Dialer] Telecom Manager feature is disabled")
if (corePreferences.manuallyDisabledTelecomManager) {
Log.w("[Dialer] User has manually disabled Telecom Manager feature")
} else {
if (Compatibility.hasTelecomManagerPermissions(requireContext())) {
enableTelecomManager()
} else {
Log.i("[Dialer] Asking for Telecom Manager permissions")
Compatibility.requestTelecomManagerPermissions(requireActivity(), 1)
}
}
} else {
Log.i("[Dialer] Telecom Manager feature is already enabled")
}
}
@TargetApi(Version.API26_O_80)
private fun enableTelecomManager() {
Log.i("[Dialer] Telecom Manager permissions granted")
if (!TelecomHelper.exists()) {
Log.i("[Dialer] Creating Telecom Helper")
TelecomHelper.create(requireContext())
} else {
Log.e("[Dialer] Telecom Manager was already created ?!")
}
corePreferences.useTelecomManager = true
}
private fun displayDebugPopup() {
val alertDialog = MaterialAlertDialogBuilder(requireContext())
alertDialog.setTitle(getString(R.string.debug_popup_title))

View file

@ -21,7 +21,6 @@ package org.linphone.activities.main.dialer.viewmodels
import android.content.Context
import android.os.Vibrator
import android.provider.Settings
import android.text.Editable
import android.widget.EditText
import androidx.lifecycle.MutableLiveData
@ -69,23 +68,8 @@ class DialerViewModel : LogsUploadViewModel() {
}
enteredUri.value = sb.toString()
if (coreContext.core.callsNb == 0) {
val contentResolver = coreContext.context.contentResolver
try {
if (Settings.System.getInt(
contentResolver,
Settings.System.DTMF_TONE_WHEN_DIALING
) == 1
) {
coreContext.core.playDtmf(key, 1)
if (vibrator.hasVibrator() && corePreferences.dtmfKeypadVibration) {
Compatibility.eventVibration(vibrator)
}
}
} catch (snfe: Settings.SettingNotFoundException) {
Log.e("[Dialer] Can't play DTMF: $snfe")
}
if (vibrator.hasVibrator() && corePreferences.dtmfKeypadVibration) {
Compatibility.eventVibration(vibrator)
}
}
@ -176,7 +160,7 @@ class DialerViewModel : LogsUploadViewModel() {
fun updateShowVideoPreview() {
val videoPreview = corePreferences.videoPreview
showPreview.value = videoPreview
coreContext.core.enableVideoPreview(videoPreview)
coreContext.core.isVideoPreviewEnabled = videoPreview
}
fun eraseLastChar() {

View file

@ -56,4 +56,8 @@ abstract class GenericViewerFragment<T : ViewDataBinding> : SecureFragment<T>()
(childFragmentManager.findFragmentById(R.id.top_bar_fragment) as? TopBarFragment)
?.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.view.View
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.R
import org.linphone.activities.GenericFragment
import org.linphone.activities.SnackBarActivity
import org.linphone.compatibility.Compatibility
import org.linphone.core.Content
import org.linphone.core.tools.Log
import org.linphone.databinding.FileViewerTopBarFragmentBinding
import org.linphone.mediastream.Version
import org.linphone.utils.FileUtils
import org.linphone.utils.PermissionHelper
class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
private var content: Content? = null
@ -46,20 +54,9 @@ class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
}
binding.setExportClickListener {
if (content != null) {
val filePath = content?.plainFilePath.orEmpty()
plainFilePath = if (filePath.isEmpty()) content?.filePath.orEmpty() else filePath
Log.i("[File Viewer] Plain file path is: $plainFilePath")
if (plainFilePath.isNotEmpty()) {
if (!FileUtils.openFileInThirdPartyApp(requireActivity(), plainFilePath)) {
(requireActivity() as SnackBarActivity).showSnackBar(R.string.chat_message_no_app_found_to_handle_file_mime_type)
if (plainFilePath != content?.filePath.orEmpty()) {
Log.i("[File Viewer] No app to open plain file path: $plainFilePath, destroying it")
FileUtils.deleteFile(plainFilePath)
}
plainFilePath = ""
}
}
val contentToExport = content
if (contentToExport != null) {
exportContent(contentToExport)
} else {
Log.e("[File Viewer] No Content set!")
}
@ -89,4 +86,77 @@ class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
content = c
binding.fileName.text = c.name
}
private fun exportContent(content: Content) {
lifecycleScope.launch {
var mediaStoreFilePath = ""
if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10) || PermissionHelper.get().hasWriteExternalStoragePermission()) {
Log.i("[File Viewer] Exporting image through Media Store API")
when (content.type) {
"image" -> {
val export = lifecycleScope.async {
Compatibility.addImageToMediaStore(requireContext(), content)
}
if (export.await()) {
Log.i("[File Viewer] Adding image ${content.name} to Media Store terminated: ${content.userData}")
mediaStoreFilePath = content.userData.toString()
} else {
Log.e("[File Viewer] Something went wrong while copying file to Media Store...")
}
}
"video" -> {
val export = lifecycleScope.async {
Compatibility.addVideoToMediaStore(requireContext(), content)
}
if (export.await()) {
Log.i("[File Viewer] Adding video ${content.name} to Media Store terminated: ${content.userData}")
mediaStoreFilePath = content.userData.toString()
} else {
Log.e("[File Viewer] Something went wrong while copying file to Media Store...")
}
}
"audio" -> {
val export = lifecycleScope.async {
Compatibility.addAudioToMediaStore(requireContext(), content)
}
if (export.await()) {
Log.i("[File Viewer] Adding audio ${content.name} to Media Store terminated: ${content.userData}")
mediaStoreFilePath = content.userData.toString()
} else {
Log.e("[File Viewer] Something went wrong while copying file to Media Store...")
}
}
else -> {
Log.w("[File Viewer] File ${content.name} isn't either an image, an audio file or a video, can't add it to the Media Store")
}
}
} else {
Log.w("[File Viewer] Can't export image through Media Store API (requires Android 10 or WRITE_EXTERNAL permission, using fallback method...")
}
withContext(Dispatchers.Main) {
if (mediaStoreFilePath.isEmpty()) {
Log.w("[File Viewer] Media store file path is empty, media store export failed?")
val filePath = content.plainFilePath.orEmpty()
plainFilePath = filePath.ifEmpty { content.filePath.orEmpty() }
Log.i("[File Viewer] Plain file path is: $plainFilePath")
if (plainFilePath.isNotEmpty()) {
if (!FileUtils.openFileInThirdPartyApp(requireActivity(), plainFilePath)) {
(requireActivity() as SnackBarActivity).showSnackBar(R.string.chat_message_no_app_found_to_handle_file_mime_type)
if (plainFilePath != content.filePath.orEmpty()) {
Log.i("[File Viewer] No app to open plain file path: $plainFilePath, destroying it")
FileUtils.deleteFile(plainFilePath)
}
plainFilePath = ""
}
}
} else {
plainFilePath = ""
Log.i("[File Viewer] Media store file path is: $mediaStoreFilePath")
FileUtils.openMediaStoreFile(requireActivity(), mediaStoreFilePath)
}
}
}
}
}

View file

@ -29,7 +29,12 @@ open class FileViewerViewModel(val content: Content) : ViewModel() {
private val deleteAfterUse: Boolean = content.isFileEncrypted
init {
filePath = if (deleteAfterUse) content.plainFilePath else content.filePath.orEmpty()
filePath = if (deleteAfterUse) {
Log.i("[File Viewer] Content is encrypted, requesting plain file path")
content.plainFilePath
} else {
content.filePath.orEmpty()
}
}
override fun onCleared() {

View file

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

View file

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

View file

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

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
import org.linphone.activities.main.history.viewmodels.CallLogViewModel
import org.linphone.core.CallLog
class GroupedCallLogData(callLog: CallLog) {
var lastCallLog: CallLog = callLog
val callLogs = arrayListOf(callLog)
val lastCallLogViewModel = CallLogViewModel(lastCallLog)
val lastCallLogData = CallLogData(lastCallLog)
fun destroy() {
lastCallLogViewModel.destroy()
lastCallLogData.destroy()
}
}

View file

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

View file

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

View file

@ -22,17 +22,16 @@ package org.linphone.activities.main.history.viewmodels
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.activities.main.history.data.CallLogData
import org.linphone.contact.GenericContactViewModel
import org.linphone.core.*
import org.linphone.core.tools.Log
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.TimestampUtils
class CallLogViewModelFactory(private val callLog: CallLog) :
ViewModelProvider.NewInstanceFactory() {
@ -48,53 +47,6 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
LinphoneUtils.getDisplayableAddress(callLog.remoteAddress)
}
val statusIconResource: Int by lazy {
if (callLog.dir == Call.Dir.Incoming) {
if (callLog.status == Call.Status.Missed) {
R.drawable.call_status_missed
} else {
R.drawable.call_status_incoming
}
} else {
R.drawable.call_status_outgoing
}
}
val iconContentDescription: Int by lazy {
if (callLog.dir == Call.Dir.Incoming) {
if (callLog.status == Call.Status.Missed) {
R.string.content_description_missed_call
} else {
R.string.content_description_incoming_call
}
} else {
R.string.content_description_outgoing_call
}
}
val directionIconResource: Int by lazy {
if (callLog.dir == Call.Dir.Incoming) {
if (callLog.status == Call.Status.Missed) {
R.drawable.call_missed
} else {
R.drawable.call_incoming
}
} else {
R.drawable.call_outgoing
}
}
val duration: String by lazy {
val dateFormat = SimpleDateFormat(if (callLog.duration >= 3600) "HH:mm:ss" else "mm:ss", Locale.getDefault())
val cal = Calendar.getInstance()
cal[0, 0, 0, 0, 0] = callLog.duration
dateFormat.format(cal.time)
}
val date: String by lazy {
TimestampUtils.toString(callLog.startDate, shortDate = false, hideYear = false)
}
val startCallEvent: MutableLiveData<Event<CallLog>> by lazy {
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 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() {
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
@ -126,14 +87,19 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
init {
waitForChatRoomCreation.value = false
coreContext.core.addListener(listener)
}
override fun onCleared() {
coreContext.core.removeListener(listener)
destroy()
super.onCleared()
}
fun destroy() {
relatedCallLogs.value.orEmpty().forEach(CallLogData::destroy)
}
fun startCall() {
@ -157,11 +123,15 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
}
}
fun getCallsHistory(): ArrayList<CallLogViewModel> {
val callsHistory = ArrayList<CallLogViewModel>()
for (callLog in relatedCallLogs.value.orEmpty()) {
callsHistory.add(CallLogViewModel(callLog))
fun addRelatedCallLogs(logs: ArrayList<CallLog>) {
val callsHistory = ArrayList<CallLogData>()
// 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
// This is for item selection through ListTopBarFragment
position = adapterPosition
position = bindingAdapterPosition
selectionListViewModel = selectionViewModel
setClickListener {
if (selectionViewModel.isEditionEnabled.value == true) {
selectionViewModel.onToggleSelect(adapterPosition)
selectionViewModel.onToggleSelect(bindingAdapterPosition)
}
}

View file

@ -164,8 +164,9 @@ class RecordingData(val path: String, private val recordingListener: RecordingLi
}
private fun initPlayer() {
// Use speaker sound card to play recordings, otherwise use earpiece
// In case no headphones/headset is connected, use speaker sound card to play recordings, otherwise use earpiece
// If none are available, default one will be used
var headphonesCard: String? = null
var speakerCard: String? = null
var earpieceCard: String? = null
for (device in coreContext.core.audioDevices) {
@ -174,11 +175,14 @@ class RecordingData(val path: String, private val recordingListener: RecordingLi
speakerCard = device.id
} else if (device.type == AudioDevice.Type.Earpiece) {
earpieceCard = device.id
} else if (device.type == AudioDevice.Type.Headphones || device.type == AudioDevice.Type.Headset) {
headphonesCard = device.id
}
}
}
Log.i("[Recording VM] Found headset/headphones sound card [$headphonesCard], speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]")
val localPlayer = coreContext.core.createLocalPlayer(speakerCard ?: earpieceCard, null, null)
val localPlayer = coreContext.core.createLocalPlayer(headphonesCard ?: speakerCard ?: earpieceCard, null, null)
if (localPlayer != null) player = localPlayer
else Log.e("[Recording VM] Couldn't create local player!")
player.addListener(listener)

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.data.RecordingData
import org.linphone.activities.main.recordings.viewmodels.RecordingsViewModel
import org.linphone.core.tools.Log
import org.linphone.databinding.RecordingsFragmentBinding
import org.linphone.utils.AppUtils
import org.linphone.utils.RecyclerViewHeaderDecoration
@ -69,11 +70,10 @@ class RecordingsFragment : MasterFragment<RecordingsFragmentBinding, RecordingsL
binding.recordingsList.addItemDecoration(headerItemDecoration)
viewModel.recordingsList.observe(
viewLifecycleOwner,
{ recordings ->
adapter.submitList(recordings)
}
)
viewLifecycleOwner
) { recordings ->
adapter.submitList(recordings)
}
binding.setBackClickListener { goBack() }
@ -111,4 +111,13 @@ class RecordingsFragment : MasterFragment<RecordingsFragmentBinding, RecordingsL
}
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 {
getRecordings()
isVideoVisible.value = false
}
@ -86,10 +85,10 @@ class RecordingsViewModel : ViewModel() {
FileUtils.deleteFile(recording.path)
}
getRecordings()
udpdateRecordingsList()
}
private fun getRecordings() {
fun udpdateRecordingsList() {
recordingsList.value.orEmpty().forEach(RecordingData::destroy)
val list = arrayListOf<RecordingData>()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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