Merge branch 'release/4.6'
This commit is contained in:
commit
b5c141f081
201 changed files with 4953 additions and 2698 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
40
CHANGELOG.md
40
CHANGELOG.md
|
@ -10,33 +10,58 @@ Group changes to describe their impact on the project, as follows:
|
|||
Fixed for any bug fixes.
|
||||
Security to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
## [4.6.0] - Unreleased
|
||||
## [4.7.0] - Unreleased
|
||||
|
||||
## [4.6.0] - 2022-02-09
|
||||
|
||||
### Added
|
||||
- Reply to chat message feature (with original message preview)
|
||||
- Swipe action on chat messages to reply / delete
|
||||
- Voice recordings in chat feature
|
||||
- Allow video recording in chat file sharing
|
||||
- Unread messages indicator in chat conversation that separates read & unread messages
|
||||
- Notify incoming/outgoing calls on bluetooth devices using self-managed connections from telecom manager API
|
||||
- Notify incoming/outgoing calls on bluetooth devices using self-managed connections from telecom manager API (disables SDK audio focus)
|
||||
- Ask Android to not process what user types in an encrypted chat room to improve privacy, see [IME_FLAG_NO_PERSONALIZED_LEARNING](https://developer.android.com/reference/android/view/inputmethod/EditorInfo#IME_FLAG_NO_PERSONALIZED_LEARNING)
|
||||
- SIP URIs in chat messages are clickable to easily initiate a call
|
||||
- New video call UI on foldable device like Galaxy Z Fold
|
||||
- Setting to automatically record all calls
|
||||
- When using a physical keyboard, use left control + enter keys to send message
|
||||
- Using CallStyle notifications for calls for devices running Android 12 or newer
|
||||
- New fragment explaining generic SIP account limitations contrary to sip.linphone.org SIP accounts
|
||||
- Link to Weblate added in about page
|
||||
|
||||
### Changed
|
||||
- UI has been reworked around SlidingPane component to better handle tablets & foldable devices
|
||||
- No longer scroll to bottom of chat room when new messages are received, a new button shows up to do it and it displays conversation's unread messages count
|
||||
- Animations have been replaced to use com.google.android.material.transition ones
|
||||
- Using new [Unified Content API](https://developer.android.com/about/versions/12/features/unified-content-api) to share files from keyboard (or other sources)
|
||||
- Received messages are now trimmed
|
||||
- Bumped dependencies, gradle updated from 4.2.2 to 7.0.2
|
||||
- Target Android SDK version set to 31 (Android 12)
|
||||
- Splashscreen is using new APIs
|
||||
- SDK updated to 5.1.0 release
|
||||
- Updated translations
|
||||
|
||||
### Fixed
|
||||
- Chat notifications disappearing when app restarts
|
||||
- "Infinite backstack", now each view is stored (at most) once in the backstack
|
||||
- Voice messages / call recordings will be played on headset/headphones instead of speaker, if possible
|
||||
- Going back to the dialer when pressing back in a chat room after clicking on a chat message notification
|
||||
- Missing international prefix / phone number in assistant after granting permission
|
||||
- Display issue for incoming call notification preventing to use answer/hangup actions on some Xiaomi devices (like Redmi Note 9S)
|
||||
- Missing foreground service notification for background mode
|
||||
|
||||
### Removed
|
||||
- Launcher Activity has been replaced by [Splash Screen API](https://developer.android.com/reference/kotlin/androidx/core/splashscreen/SplashScreen)
|
||||
- Dialer will no longer make DTMF sound when pressing digits
|
||||
- Launcher activity
|
||||
- Global push notification setting in Network, use the switch in each Account instead
|
||||
- No longer need to monitor device rotation and give information to the Core, it does it by itself
|
||||
|
||||
## [4.5.6] - 2021-11-08
|
||||
|
||||
### Changed
|
||||
- SDK updated to 5.0.49
|
||||
|
||||
## [4.5.5] - 2021-10-28
|
||||
|
||||
|
@ -79,16 +104,11 @@ Group changes to describe their impact on the project, as follows:
|
|||
- Fixed various crashes & other issues
|
||||
- SDK bumped to 5.0.10
|
||||
|
||||
## [4.5.1] - Unreleased
|
||||
|
||||
### Added
|
||||
- Reply to chat message feature
|
||||
- Voice recordings messages
|
||||
## [4.5.1] - 2021-07-15
|
||||
|
||||
### Changed
|
||||
- Navigation was reworked using SlidingPane widget, reducing code & improving UI on foldables
|
||||
|
||||
### Removed
|
||||
- Bugs & crashes have been fixed
|
||||
- SDK bumped to 5.0.1
|
||||
|
||||
## [4.5.0] - 2021-07-08
|
||||
|
||||
|
|
33
README.md
33
README.md
|
@ -97,7 +97,9 @@ Also check you have built the SDK for the right CPU architecture using the `-DLI
|
|||
|
||||
- Push notification might not work when app has been started by Android Studio consecutively to an install. Remove the app from the recent activity view and start it again using the launcher icon to resolve this.
|
||||
|
||||
## Troubleshouting
|
||||
## Troubleshooting
|
||||
|
||||
### Behavior issue
|
||||
|
||||
When submitting an issue on our [Github repository](https://github.com/BelledonneCommunications/linphone-android), please follow the template and attach the matching library logs:
|
||||
|
||||
|
@ -105,7 +107,32 @@ When submitting an issue on our [Github repository](https://github.com/Belledonn
|
|||
|
||||
2. Then restart the app, reproduce the issue and upload the logs using the `Send logs` button on the About page.
|
||||
|
||||
3. Finally paste the link to the uploaded logs (link is already in the clipboard after a sucessful upload).
|
||||
3. Finally paste the link to the uploaded logs (link is already in the clipboard after a successful upload).
|
||||
|
||||
### Native crash
|
||||
|
||||
First of all, to be able to get a symbolized stack trace, you need the debug version of our libraries.
|
||||
|
||||
If you haven't built the SDK locally (see [building a local SDK](#BuildingalocalSDK)), here's how to get them:
|
||||
|
||||
1. Go to our [maven repository](https://download.linphone.org/maven_repository/org/linphone/linphone-sdk-android-debug/), in the linphone-android-debug directory.
|
||||
|
||||
2. Download the AAR file with **the exact same version** as the AAR that was used to generate the crash's stacktrace.
|
||||
|
||||
3. Extract the AAR somewhere on your computer (it's a simple ZIP file even it's doesn't have the extension). Libraries are stored inside the ```jni``` folder (a directory for each architectured built, usually ```arm64-v8a, armeabi-v7a, x86_64 and x86```).
|
||||
|
||||
4. To get consistent with locally built SDK, rename the ```jni``` directory into ```libs-debug```.
|
||||
|
||||
Now you need the ```ndk-stack``` tool and possibly ```adb logcat```.
|
||||
|
||||
If your computer isn't used for Android development, you can download those tools from [Google website](https://developer.android.com/studio#downloads), in the ```Command line tools only``` section.
|
||||
|
||||
Once you have the debug libraries and the proper tools installed, you can use the ```ndk-stack``` tool to symbolize your stacktrace. Note that you also need to know the architecture (armv7, arm64, x86, etc...) of the libraries that were used.
|
||||
|
||||
Here's how to get the stacktrace and the right architecture from a device plugged to your computer:
|
||||
```
|
||||
adb logcat -d | ndk-stack -sym ./libs-debug/`adb shell getprop ro.product.cpu.abi | tr -d '\r'`
|
||||
```
|
||||
|
||||
## Create an APK with a different package name
|
||||
|
||||
|
@ -137,6 +164,6 @@ Due to the full app rewrite we can't re-use previous translations, so we'll be v
|
|||
In order to submit a patch for inclusion in linphone's source code:
|
||||
|
||||
1. First make sure your patch applies to latest git sources before submitting: patches made to old versions can't and won't be merged.
|
||||
2. Fill out and send us an email with the link of pullrequest and the [Contributor Agreement](https://linphone.org/sites/default/files/bc-contributor-agreement_0.pdf) for your patch to be included in the git tree.
|
||||
2. Fill out and send us an email with the link of pull-request and the [Contributor Agreement](https://linphone.org/sites/default/files/bc-contributor-agreement_0.pdf) for your patch to be included in the git tree.
|
||||
|
||||
The goal of this agreement to grant us peaceful exercise of our rights on the linphone source code, while not losing your rights on your contribution.
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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?) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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?) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>()
|
||||
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue