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/debug
|
||||||
app/release
|
app/release
|
||||||
app/releaseAppBundle
|
app/releaseAppBundle
|
||||||
|
app/releaseWithCrashlytics
|
||||||
keystore.properties
|
keystore.properties
|
||||||
app/src/main/res/xml/contacts.xml
|
app/src/main/res/xml/contacts.xml
|
||||||
|
|
|
@ -7,6 +7,7 @@ job-android:
|
||||||
before_script:
|
before_script:
|
||||||
- if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then eval $(ssh-agent -s); fi
|
- if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then eval $(ssh-agent -s); fi
|
||||||
- if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then echo "$SCP_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null; fi
|
- if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then echo "$SCP_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null; fi
|
||||||
|
- echo "$ANDROID_SETTINGS_GRADLE" > settings.gradle
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- sdkmanager
|
- sdkmanager
|
||||||
|
|
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.
|
Fixed for any bug fixes.
|
||||||
Security to invite users to upgrade in case of vulnerabilities.
|
Security to invite users to upgrade in case of vulnerabilities.
|
||||||
|
|
||||||
## [4.6.0] - Unreleased
|
## [4.7.0] - Unreleased
|
||||||
|
|
||||||
|
## [4.6.0] - 2022-02-09
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Reply to chat message feature (with original message preview)
|
- Reply to chat message feature (with original message preview)
|
||||||
|
- Swipe action on chat messages to reply / delete
|
||||||
- Voice recordings in chat feature
|
- Voice recordings in chat feature
|
||||||
- Allow video recording in chat file sharing
|
- Allow video recording in chat file sharing
|
||||||
- Unread messages indicator in chat conversation that separates read & unread messages
|
- Unread messages indicator in chat conversation that separates read & unread messages
|
||||||
- Notify incoming/outgoing calls on bluetooth devices using self-managed connections from telecom manager API
|
- Notify incoming/outgoing calls on bluetooth devices using self-managed connections from telecom manager API (disables SDK audio focus)
|
||||||
|
- Ask Android to not process what user types in an encrypted chat room to improve privacy, see [IME_FLAG_NO_PERSONALIZED_LEARNING](https://developer.android.com/reference/android/view/inputmethod/EditorInfo#IME_FLAG_NO_PERSONALIZED_LEARNING)
|
||||||
|
- SIP URIs in chat messages are clickable to easily initiate a call
|
||||||
- New video call UI on foldable device like Galaxy Z Fold
|
- New video call UI on foldable device like Galaxy Z Fold
|
||||||
- Setting to automatically record all calls
|
- Setting to automatically record all calls
|
||||||
|
- When using a physical keyboard, use left control + enter keys to send message
|
||||||
|
- Using CallStyle notifications for calls for devices running Android 12 or newer
|
||||||
|
- New fragment explaining generic SIP account limitations contrary to sip.linphone.org SIP accounts
|
||||||
|
- Link to Weblate added in about page
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- UI has been reworked around SlidingPane component to better handle tablets & foldable devices
|
- UI has been reworked around SlidingPane component to better handle tablets & foldable devices
|
||||||
- No longer scroll to bottom of chat room when new messages are received, a new button shows up to do it and it displays conversation's unread messages count
|
- No longer scroll to bottom of chat room when new messages are received, a new button shows up to do it and it displays conversation's unread messages count
|
||||||
- Animations have been replaced to use com.google.android.material.transition ones
|
- Animations have been replaced to use com.google.android.material.transition ones
|
||||||
- Using new [Unified Content API](https://developer.android.com/about/versions/12/features/unified-content-api) to share files from keyboard (or other sources)
|
- Using new [Unified Content API](https://developer.android.com/about/versions/12/features/unified-content-api) to share files from keyboard (or other sources)
|
||||||
|
- Received messages are now trimmed
|
||||||
- Bumped dependencies, gradle updated from 4.2.2 to 7.0.2
|
- Bumped dependencies, gradle updated from 4.2.2 to 7.0.2
|
||||||
- Target Android SDK version set to 31 (Android 12)
|
- Target Android SDK version set to 31 (Android 12)
|
||||||
|
- Splashscreen is using new APIs
|
||||||
- SDK updated to 5.1.0 release
|
- SDK updated to 5.1.0 release
|
||||||
|
- Updated translations
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Chat notifications disappearing when app restarts
|
- Chat notifications disappearing when app restarts
|
||||||
- "Infinite backstack", now each view is stored (at most) once in the backstack
|
- "Infinite backstack", now each view is stored (at most) once in the backstack
|
||||||
|
- Voice messages / call recordings will be played on headset/headphones instead of speaker, if possible
|
||||||
- Going back to the dialer when pressing back in a chat room after clicking on a chat message notification
|
- Going back to the dialer when pressing back in a chat room after clicking on a chat message notification
|
||||||
|
- Missing international prefix / phone number in assistant after granting permission
|
||||||
|
- Display issue for incoming call notification preventing to use answer/hangup actions on some Xiaomi devices (like Redmi Note 9S)
|
||||||
|
- Missing foreground service notification for background mode
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
- Launcher Activity has been replaced by [Splash Screen API](https://developer.android.com/reference/kotlin/androidx/core/splashscreen/SplashScreen)
|
||||||
|
- Dialer will no longer make DTMF sound when pressing digits
|
||||||
|
- Launcher activity
|
||||||
- Global push notification setting in Network, use the switch in each Account instead
|
- Global push notification setting in Network, use the switch in each Account instead
|
||||||
|
- No longer need to monitor device rotation and give information to the Core, it does it by itself
|
||||||
|
|
||||||
|
## [4.5.6] - 2021-11-08
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- SDK updated to 5.0.49
|
||||||
|
|
||||||
## [4.5.5] - 2021-10-28
|
## [4.5.5] - 2021-10-28
|
||||||
|
|
||||||
|
@ -79,16 +104,11 @@ Group changes to describe their impact on the project, as follows:
|
||||||
- Fixed various crashes & other issues
|
- Fixed various crashes & other issues
|
||||||
- SDK bumped to 5.0.10
|
- SDK bumped to 5.0.10
|
||||||
|
|
||||||
## [4.5.1] - Unreleased
|
## [4.5.1] - 2021-07-15
|
||||||
|
|
||||||
### Added
|
|
||||||
- Reply to chat message feature
|
|
||||||
- Voice recordings messages
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Navigation was reworked using SlidingPane widget, reducing code & improving UI on foldables
|
- Bugs & crashes have been fixed
|
||||||
|
- SDK bumped to 5.0.1
|
||||||
### Removed
|
|
||||||
|
|
||||||
## [4.5.0] - 2021-07-08
|
## [4.5.0] - 2021-07-08
|
||||||
|
|
||||||
|
|
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.
|
- Push notification might not work when app has been started by Android Studio consecutively to an install. Remove the app from the recent activity view and start it again using the launcher icon to resolve this.
|
||||||
|
|
||||||
## Troubleshouting
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Behavior issue
|
||||||
|
|
||||||
When submitting an issue on our [Github repository](https://github.com/BelledonneCommunications/linphone-android), please follow the template and attach the matching library logs:
|
When submitting an issue on our [Github repository](https://github.com/BelledonneCommunications/linphone-android), please follow the template and attach the matching library logs:
|
||||||
|
|
||||||
|
@ -105,7 +107,32 @@ When submitting an issue on our [Github repository](https://github.com/Belledonn
|
||||||
|
|
||||||
2. Then restart the app, reproduce the issue and upload the logs using the `Send logs` button on the About page.
|
2. Then restart the app, reproduce the issue and upload the logs using the `Send logs` button on the About page.
|
||||||
|
|
||||||
3. Finally paste the link to the uploaded logs (link is already in the clipboard after a sucessful upload).
|
3. Finally paste the link to the uploaded logs (link is already in the clipboard after a successful upload).
|
||||||
|
|
||||||
|
### Native crash
|
||||||
|
|
||||||
|
First of all, to be able to get a symbolized stack trace, you need the debug version of our libraries.
|
||||||
|
|
||||||
|
If you haven't built the SDK locally (see [building a local SDK](#BuildingalocalSDK)), here's how to get them:
|
||||||
|
|
||||||
|
1. Go to our [maven repository](https://download.linphone.org/maven_repository/org/linphone/linphone-sdk-android-debug/), in the linphone-android-debug directory.
|
||||||
|
|
||||||
|
2. Download the AAR file with **the exact same version** as the AAR that was used to generate the crash's stacktrace.
|
||||||
|
|
||||||
|
3. Extract the AAR somewhere on your computer (it's a simple ZIP file even it's doesn't have the extension). Libraries are stored inside the ```jni``` folder (a directory for each architectured built, usually ```arm64-v8a, armeabi-v7a, x86_64 and x86```).
|
||||||
|
|
||||||
|
4. To get consistent with locally built SDK, rename the ```jni``` directory into ```libs-debug```.
|
||||||
|
|
||||||
|
Now you need the ```ndk-stack``` tool and possibly ```adb logcat```.
|
||||||
|
|
||||||
|
If your computer isn't used for Android development, you can download those tools from [Google website](https://developer.android.com/studio#downloads), in the ```Command line tools only``` section.
|
||||||
|
|
||||||
|
Once you have the debug libraries and the proper tools installed, you can use the ```ndk-stack``` tool to symbolize your stacktrace. Note that you also need to know the architecture (armv7, arm64, x86, etc...) of the libraries that were used.
|
||||||
|
|
||||||
|
Here's how to get the stacktrace and the right architecture from a device plugged to your computer:
|
||||||
|
```
|
||||||
|
adb logcat -d | ndk-stack -sym ./libs-debug/`adb shell getprop ro.product.cpu.abi | tr -d '\r'`
|
||||||
|
```
|
||||||
|
|
||||||
## Create an APK with a different package name
|
## Create an APK with a different package name
|
||||||
|
|
||||||
|
@ -137,6 +164,6 @@ Due to the full app rewrite we can't re-use previous translations, so we'll be v
|
||||||
In order to submit a patch for inclusion in linphone's source code:
|
In order to submit a patch for inclusion in linphone's source code:
|
||||||
|
|
||||||
1. First make sure your patch applies to latest git sources before submitting: patches made to old versions can't and won't be merged.
|
1. First make sure your patch applies to latest git sources before submitting: patches made to old versions can't and won't be merged.
|
||||||
2. Fill out and send us an email with the link of pullrequest and the [Contributor Agreement](https://linphone.org/sites/default/files/bc-contributor-agreement_0.pdf) for your patch to be included in the git tree.
|
2. Fill out and send us an email with the link of pull-request and the [Contributor Agreement](https://linphone.org/sites/default/files/bc-contributor-agreement_0.pdf) for your patch to be included in the git tree.
|
||||||
|
|
||||||
The goal of this agreement to grant us peaceful exercise of our rights on the linphone source code, while not losing your rights on your contribution.
|
The goal of this agreement to grant us peaceful exercise of our rights on the linphone source code, while not losing your rights on your contribution.
|
||||||
|
|
|
@ -5,6 +5,9 @@ plugins {
|
||||||
id 'org.jlleitschuh.gradle.ktlint'
|
id 'org.jlleitschuh.gradle.ktlint'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def appVersionName = "4.6.0"
|
||||||
|
def appVersionCode = 40600 // 4.06.00
|
||||||
|
|
||||||
static def getPackageName() {
|
static def getPackageName() {
|
||||||
return "org.linphone"
|
return "org.linphone"
|
||||||
}
|
}
|
||||||
|
@ -24,7 +27,7 @@ if (crashlyticsEnabled) {
|
||||||
|
|
||||||
def gitBranch = new ByteArrayOutputStream()
|
def gitBranch = new ByteArrayOutputStream()
|
||||||
task getGitVersion() {
|
task getGitVersion() {
|
||||||
def gitVersion = "4.7.0"
|
def gitVersion = appVersionName
|
||||||
def gitVersionStream = new ByteArrayOutputStream()
|
def gitVersionStream = new ByteArrayOutputStream()
|
||||||
def gitCommitsCount = new ByteArrayOutputStream()
|
def gitCommitsCount = new ByteArrayOutputStream()
|
||||||
def gitCommitHash = new ByteArrayOutputStream()
|
def gitCommitHash = new ByteArrayOutputStream()
|
||||||
|
@ -52,9 +55,9 @@ task getGitVersion() {
|
||||||
} else {
|
} else {
|
||||||
gitVersion = gitVersionStream.toString().trim() + "." + gitCommitsCount.toString().trim() + "+" + gitCommitHash.toString().trim()
|
gitVersion = gitVersionStream.toString().trim() + "." + gitCommitsCount.toString().trim() + "+" + gitCommitHash.toString().trim()
|
||||||
}
|
}
|
||||||
println("Git version: " + gitVersion)
|
println("Git version: " + gitVersion + " (" + appVersionCode + ")")
|
||||||
} catch (ignored) {
|
} catch (ignored) {
|
||||||
println("Git not found")
|
println("Git not found, using " + gitVersion + " (" + appVersionCode + ")")
|
||||||
}
|
}
|
||||||
project.version = gitVersion
|
project.version = gitVersion
|
||||||
}
|
}
|
||||||
|
@ -84,7 +87,7 @@ android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 31
|
targetSdkVersion 31
|
||||||
versionCode 4700
|
versionCode appVersionCode
|
||||||
versionName "${project.version}"
|
versionName "${project.version}"
|
||||||
applicationId getPackageName()
|
applicationId getPackageName()
|
||||||
}
|
}
|
||||||
|
@ -95,7 +98,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
// See https://developer.android.com/studio/releases/gradle-plugin#3-6-0-behavior for why extractNativeLibs is set to true in debug flavor
|
// See https://developer.android.com/studio/releases/gradle-plugin#3-6-0-behavior for why extractNativeLibs is set to true in debug flavor
|
||||||
if (variant.buildType.name == "release" || variant.buildType.name == "releaseAppBundle") {
|
if (variant.buildType.name == "release" || variant.buildType.name == "releaseWithCrashlytics") {
|
||||||
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
|
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
|
||||||
linphone_file_provider: getPackageName() + ".fileprovider",
|
linphone_file_provider: getPackageName() + ".fileprovider",
|
||||||
appLabel: "@string/app_name",
|
appLabel: "@string/app_name",
|
||||||
|
@ -143,8 +146,8 @@ android {
|
||||||
initWith release
|
initWith release
|
||||||
|
|
||||||
resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString()
|
resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString()
|
||||||
if (crashlyticsEnabled) {
|
|
||||||
|
|
||||||
|
if (crashlyticsEnabled) {
|
||||||
firebaseCrashlytics {
|
firebaseCrashlytics {
|
||||||
nativeSymbolUploadEnabled true
|
nativeSymbolUploadEnabled true
|
||||||
unstrippedNativeLibsDir file(LinphoneSdkBuildDir + '/libs-debug/').toString()
|
unstrippedNativeLibsDir file(LinphoneSdkBuildDir + '/libs-debug/').toString()
|
||||||
|
@ -161,14 +164,13 @@ android {
|
||||||
resValue "string", "sync_account_type", getPackageName() + ".sync"
|
resValue "string", "sync_account_type", getPackageName() + ".sync"
|
||||||
resValue "string", "file_provider", getPackageName() + ".debug.fileprovider"
|
resValue "string", "file_provider", getPackageName() + ".debug.fileprovider"
|
||||||
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"
|
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"
|
||||||
|
resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString()
|
||||||
|
|
||||||
if (!firebaseEnabled) {
|
if (!firebaseEnabled) {
|
||||||
resValue "string", "gcm_defaultSenderId", "none"
|
resValue "string", "gcm_defaultSenderId", "none"
|
||||||
}
|
}
|
||||||
|
|
||||||
resValue "bool", "crashlytics_enabled", crashlyticsEnabled.toString()
|
|
||||||
if (crashlyticsEnabled) {
|
if (crashlyticsEnabled) {
|
||||||
|
|
||||||
firebaseCrashlytics {
|
firebaseCrashlytics {
|
||||||
nativeSymbolUploadEnabled true
|
nativeSymbolUploadEnabled true
|
||||||
unstrippedNativeLibsDir file(LinphoneSdkBuildDir + '/libs-debug/').toString()
|
unstrippedNativeLibsDir file(LinphoneSdkBuildDir + '/libs-debug/').toString()
|
||||||
|
@ -191,47 +193,28 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
|
||||||
maven {
|
|
||||||
name "local linphone-sdk maven repository"
|
|
||||||
url file(LinphoneSdkBuildDir + '/maven_repository/')
|
|
||||||
content {
|
|
||||||
includeGroup "org.linphone"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
maven {
|
|
||||||
name "linphone.org maven repository"
|
|
||||||
url "https://download.linphone.org/maven_repository"
|
|
||||||
content {
|
|
||||||
includeGroup "org.linphone"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
|
||||||
implementation 'androidx.media:media:1.4.3'
|
implementation 'androidx.media:media:1.4.3'
|
||||||
implementation 'androidx.fragment:fragment-ktx:1.4.0-beta01'
|
implementation 'androidx.fragment:fragment-ktx:1.4.1'
|
||||||
implementation 'androidx.core:core-ktx:1.7.0'
|
implementation 'androidx.core:core-ktx:1.7.0'
|
||||||
|
|
||||||
def nav_version = "2.4.0-beta01"
|
def nav_version = "2.4.0"
|
||||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||||
|
|
||||||
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0-beta01"
|
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
|
||||||
implementation "androidx.window:window:1.0.0-beta03"
|
implementation "androidx.window:window:1.0.0"
|
||||||
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||||
implementation "androidx.security:security-crypto-ktx:1.1.0-alpha03"
|
implementation "androidx.security:security-crypto-ktx:1.1.0-alpha03"
|
||||||
|
implementation 'androidx.core:core-splashscreen:1.0.0-beta01'
|
||||||
|
|
||||||
implementation 'com.google.android.material:material:1.4.0'
|
implementation 'com.google.android.material:material:1.5.0'
|
||||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||||
|
|
||||||
implementation 'androidx.emoji:emoji:1.1.0'
|
implementation 'androidx.emoji:emoji:1.1.0'
|
||||||
|
|
|
@ -42,25 +42,21 @@
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:allowNativeHeapPointerTagging="false">
|
android:allowNativeHeapPointerTagging="false">
|
||||||
|
|
||||||
<activity
|
<activity android:name=".activities.main.MainActivity"
|
||||||
android:name=".activities.launcher.LauncherActivity"
|
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:noHistory="true"
|
android:launchMode="singleTask"
|
||||||
android:theme="@style/AppTheme">
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:theme="@style/AppSplashScreenTheme">
|
||||||
|
<nav-graph android:value="@navigation/main_nav_graph" />
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.app.shortcuts"
|
android:name="android.app.shortcuts"
|
||||||
android:resource="@xml/shortcuts" />
|
android:resource="@xml/shortcuts" />
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity android:name=".activities.main.MainActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:windowSoftInputMode="adjustResize">
|
|
||||||
<nav-graph android:value="@navigation/main_nav_graph" />
|
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW_LOCUS" />
|
<action android:name="android.intent.action.VIEW_LOCUS" />
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
<entry name="realm" overwrite="true"></entry>
|
<entry name="realm" overwrite="true"></entry>
|
||||||
<entry name="conference_factory_uri" overwrite="true"></entry>
|
<entry name="conference_factory_uri" overwrite="true"></entry>
|
||||||
<entry name="push_notification_allowed" overwrite="true">0</entry>
|
<entry name="push_notification_allowed" overwrite="true">0</entry>
|
||||||
|
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">0</entry>
|
||||||
</section>
|
</section>
|
||||||
<section name="nat_policy_default_values">
|
<section name="nat_policy_default_values">
|
||||||
<entry name="stun_server" overwrite="true"></entry>
|
<entry name="stun_server" overwrite="true"></entry>
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
<entry name="realm" overwrite="true">sip.linphone.org</entry>
|
<entry name="realm" overwrite="true">sip.linphone.org</entry>
|
||||||
<entry name="conference_factory_uri" overwrite="true">sip:conference-factory@sip.linphone.org</entry>
|
<entry name="conference_factory_uri" overwrite="true">sip:conference-factory@sip.linphone.org</entry>
|
||||||
<entry name="push_notification_allowed" overwrite="true">1</entry>
|
<entry name="push_notification_allowed" overwrite="true">1</entry>
|
||||||
|
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">1</entry>
|
||||||
</section>
|
</section>
|
||||||
<section name="nat_policy_default_values">
|
<section name="nat_policy_default_values">
|
||||||
<entry name="stun_server" overwrite="true">stun.linphone.org</entry>
|
<entry name="stun_server" overwrite="true">stun.linphone.org</entry>
|
||||||
|
|
|
@ -25,13 +25,12 @@ import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import android.view.Display
|
import android.view.Display
|
||||||
import android.view.Surface
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.ActivityNavigator
|
import androidx.navigation.ActivityNavigator
|
||||||
import androidx.window.layout.FoldingFeature
|
import androidx.window.layout.FoldingFeature
|
||||||
import androidx.window.layout.WindowInfoRepository.Companion.windowInfoRepository
|
import androidx.window.layout.WindowInfoTracker
|
||||||
import androidx.window.layout.WindowLayoutInfo
|
import androidx.window.layout.WindowLayoutInfo
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -58,7 +57,10 @@ abstract class GenericActivity : AppCompatActivity() {
|
||||||
ensureCoreExists(applicationContext)
|
ensureCoreExists(applicationContext)
|
||||||
|
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
windowInfoRepository().windowLayoutInfo.collect { newLayoutInfo ->
|
WindowInfoTracker
|
||||||
|
.getOrCreate(this@GenericActivity)
|
||||||
|
.windowLayoutInfo(this@GenericActivity)
|
||||||
|
.collect { newLayoutInfo ->
|
||||||
updateCurrentLayout(newLayoutInfo)
|
updateCurrentLayout(newLayoutInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,18 +99,6 @@ abstract class GenericActivity : AppCompatActivity() {
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
var degrees = 270
|
|
||||||
val orientation = windowManager.defaultDisplay.rotation
|
|
||||||
when (orientation) {
|
|
||||||
Surface.ROTATION_0 -> degrees = 0
|
|
||||||
Surface.ROTATION_90 -> degrees = 270
|
|
||||||
Surface.ROTATION_180 -> degrees = 180
|
|
||||||
Surface.ROTATION_270 -> degrees = 90
|
|
||||||
}
|
|
||||||
Log.i("[Generic Activity] Device orientation is $degrees (raw value is $orientation)")
|
|
||||||
val rotation = (360 - degrees) % 360
|
|
||||||
coreContext.core.deviceRotation = rotation
|
|
||||||
|
|
||||||
// Remove service notification if it has been started by device boot
|
// Remove service notification if it has been started by device boot
|
||||||
coreContext.notificationsManager.stopForegroundNotificationIfPossible()
|
coreContext.notificationsManager.stopForegroundNotificationIfPossible()
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,8 +28,12 @@ import androidx.core.view.doOnPreDraw
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
import com.google.android.material.transition.MaterialSharedAxis
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
|
|
||||||
|
@ -38,11 +42,19 @@ abstract class GenericFragment<T : ViewDataBinding> : Fragment() {
|
||||||
protected val binding get() = _binding!!
|
protected val binding get() = _binding!!
|
||||||
protected var useMaterialSharedAxisXForwardAnimation = true
|
protected var useMaterialSharedAxisXForwardAnimation = true
|
||||||
|
|
||||||
|
protected fun isBindingAvailable(): Boolean {
|
||||||
|
return _binding != null
|
||||||
|
}
|
||||||
|
|
||||||
protected val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
protected val onBackPressedCallback = object : OnBackPressedCallback(true) {
|
||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
goBack()
|
goBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
abstract fun getLayoutId(): Int
|
abstract fun getLayoutId(): Int
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,30 @@ internal fun MainActivity.navigateToDialer(args: Bundle?) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun MainActivity.navigateToChatRooms(args: Bundle? = null) {
|
||||||
|
findNavController(R.id.nav_host_fragment).navigate(
|
||||||
|
R.id.action_global_masterChatRoomsFragment,
|
||||||
|
args,
|
||||||
|
popupTo(R.id.masterChatRoomsFragment, true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun MainActivity.navigateToChatRoom(localAddress: String?, peerAddress: String?) {
|
||||||
|
val deepLink = "linphone-android://chat-room/$localAddress/$peerAddress"
|
||||||
|
findNavController(R.id.nav_host_fragment).navigate(
|
||||||
|
Uri.parse(deepLink),
|
||||||
|
popupTo(R.id.masterChatRoomsFragment, true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun MainActivity.navigateToContact(contactId: String?) {
|
||||||
|
val deepLink = "linphone-android://contact/view/$contactId"
|
||||||
|
findNavController(R.id.nav_host_fragment).navigate(
|
||||||
|
Uri.parse(deepLink),
|
||||||
|
popupTo(R.id.masterContactsFragment, true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/* Tabs fragment related */
|
/* Tabs fragment related */
|
||||||
|
|
||||||
internal fun TabsFragment.navigateToCallHistory() {
|
internal fun TabsFragment.navigateToCallHistory() {
|
||||||
|
@ -79,9 +103,8 @@ internal fun TabsFragment.navigateToCallHistory() {
|
||||||
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_masterCallLogsFragment
|
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_masterCallLogsFragment
|
||||||
R.id.dialerFragment -> R.id.action_dialerFragment_to_masterCallLogsFragment
|
R.id.dialerFragment -> R.id.action_dialerFragment_to_masterCallLogsFragment
|
||||||
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_masterCallLogsFragment
|
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_masterCallLogsFragment
|
||||||
else -> 0
|
else -> R.id.action_global_masterCallLogsFragment
|
||||||
}
|
}
|
||||||
if (action == 0) return
|
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
action,
|
action,
|
||||||
null,
|
null,
|
||||||
|
@ -94,9 +117,8 @@ internal fun TabsFragment.navigateToContacts() {
|
||||||
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_masterContactsFragment
|
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_masterContactsFragment
|
||||||
R.id.dialerFragment -> R.id.action_dialerFragment_to_masterContactsFragment
|
R.id.dialerFragment -> R.id.action_dialerFragment_to_masterContactsFragment
|
||||||
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_masterContactsFragment
|
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_masterContactsFragment
|
||||||
else -> 0
|
else -> R.id.action_global_masterContactsFragment
|
||||||
}
|
}
|
||||||
if (action == 0) return
|
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
action,
|
action,
|
||||||
null,
|
null,
|
||||||
|
@ -109,9 +131,8 @@ internal fun TabsFragment.navigateToDialer() {
|
||||||
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_dialerFragment
|
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_dialerFragment
|
||||||
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_dialerFragment
|
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_dialerFragment
|
||||||
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_dialerFragment
|
R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_dialerFragment
|
||||||
else -> 0
|
else -> R.id.action_global_dialerFragment
|
||||||
}
|
}
|
||||||
if (action == 0) return
|
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
action,
|
action,
|
||||||
null,
|
null,
|
||||||
|
@ -124,9 +145,8 @@ internal fun TabsFragment.navigateToChatRooms() {
|
||||||
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_masterChatRoomsFragment
|
R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_masterChatRoomsFragment
|
||||||
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_masterChatRoomsFragment
|
R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_masterChatRoomsFragment
|
||||||
R.id.dialerFragment -> R.id.action_dialerFragment_to_masterChatRoomsFragment
|
R.id.dialerFragment -> R.id.action_dialerFragment_to_masterChatRoomsFragment
|
||||||
else -> 0
|
else -> R.id.action_global_masterChatRoomsFragment
|
||||||
}
|
}
|
||||||
if (action == 0) return
|
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
action,
|
action,
|
||||||
null,
|
null,
|
||||||
|
@ -298,7 +318,15 @@ internal fun DetailChatRoomFragment.navigateToEmptyChatRoom() {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
R.id.action_global_emptyChatFragment,
|
R.id.action_global_emptyChatFragment,
|
||||||
null,
|
null,
|
||||||
popupTo(R.id.emptyChatFragment, true)
|
popupTo(R.id.detailChatRoomFragment, true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun DetailChatRoomFragment.navigateToDialer(args: Bundle?) {
|
||||||
|
findMasterNavController().navigate(
|
||||||
|
R.id.action_global_dialerFragment,
|
||||||
|
args,
|
||||||
|
popupTo(R.id.dialerFragment, true)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,7 +345,7 @@ internal fun ChatRoomCreationFragment.navigateToChatRoom(args: Bundle) {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
R.id.action_chatRoomCreationFragment_to_detailChatRoomFragment,
|
R.id.action_chatRoomCreationFragment_to_detailChatRoomFragment,
|
||||||
args,
|
args,
|
||||||
popupTo(R.id.detailChatRoomFragment, true)
|
popupTo(R.id.chatRoomCreationFragment, true)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -326,7 +354,7 @@ internal fun ChatRoomCreationFragment.navigateToEmptyChatRoom() {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
R.id.action_global_emptyChatFragment,
|
R.id.action_global_emptyChatFragment,
|
||||||
null,
|
null,
|
||||||
popupTo(R.id.emptyChatFragment, true)
|
popupTo(R.id.chatRoomCreationFragment, true)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -782,10 +810,10 @@ internal fun WelcomeFragment.navigateToAccountLogin() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun WelcomeFragment.navigateToGenericLogin() {
|
internal fun WelcomeFragment.navigateToGenericLoginWarning() {
|
||||||
if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
|
if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
R.id.action_welcomeFragment_to_genericAccountLoginFragment,
|
R.id.action_welcomeFragment_to_genericAccountWarningFragment,
|
||||||
null,
|
null,
|
||||||
popupTo()
|
popupTo()
|
||||||
)
|
)
|
||||||
|
@ -822,6 +850,16 @@ internal fun AccountLoginFragment.navigateToPhoneAccountValidation(args: Bundle?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun GenericAccountWarningFragment.navigateToGenericLogin() {
|
||||||
|
if (findNavController().currentDestination?.id == R.id.genericAccountWarningFragment) {
|
||||||
|
findNavController().navigate(
|
||||||
|
R.id.action_genericAccountWarningFragment_to_genericAccountLoginFragment,
|
||||||
|
null,
|
||||||
|
popupTo(R.id.welcomeFragment, popUpInclusive = false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal fun GenericAccountLoginFragment.navigateToEchoCancellerCalibration() {
|
internal fun GenericAccountLoginFragment.navigateToEchoCancellerCalibration() {
|
||||||
if (findNavController().currentDestination?.id == R.id.genericAccountLoginFragment) {
|
if (findNavController().currentDestination?.id == R.id.genericAccountLoginFragment) {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
|
|
|
@ -21,5 +21,6 @@ package org.linphone.activities
|
||||||
|
|
||||||
interface SnackBarActivity {
|
interface SnackBarActivity {
|
||||||
fun showSnackBar(resourceId: Int)
|
fun showSnackBar(resourceId: Int)
|
||||||
|
fun showSnackBar(resourceId: Int, action: Int, listener: () -> Unit)
|
||||||
fun showSnackBar(message: String)
|
fun showSnackBar(message: String)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import android.os.Bundle
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||||
import org.linphone.R
|
import org.linphone.R
|
||||||
import org.linphone.activities.GenericActivity
|
import org.linphone.activities.GenericActivity
|
||||||
import org.linphone.activities.SnackBarActivity
|
import org.linphone.activities.SnackBarActivity
|
||||||
|
@ -40,12 +41,23 @@ class AssistantActivity : GenericActivity(), SnackBarActivity {
|
||||||
sharedViewModel = ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
sharedViewModel = ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||||
|
|
||||||
coordinator = findViewById(R.id.coordinator)
|
coordinator = findViewById(R.id.coordinator)
|
||||||
|
|
||||||
|
corePreferences.firstStart = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showSnackBar(resourceId: Int) {
|
override fun showSnackBar(resourceId: Int) {
|
||||||
Snackbar.make(coordinator, resourceId, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(coordinator, resourceId, Snackbar.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun showSnackBar(resourceId: Int, action: Int, listener: () -> Unit) {
|
||||||
|
Snackbar
|
||||||
|
.make(findViewById(R.id.coordinator), resourceId, Snackbar.LENGTH_LONG)
|
||||||
|
.setAction(action) {
|
||||||
|
listener()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
override fun showSnackBar(message: String) {
|
override fun showSnackBar(message: String) {
|
||||||
Snackbar.make(coordinator, message, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(coordinator, message, Snackbar.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
|
|
||||||
package org.linphone.activities.assistant.fragments
|
package org.linphone.activities.assistant.fragments
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import androidx.databinding.ViewDataBinding
|
import androidx.databinding.ViewDataBinding
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
@ -28,10 +29,15 @@ import org.linphone.activities.GenericFragment
|
||||||
import org.linphone.activities.assistant.viewmodels.AbstractPhoneViewModel
|
import org.linphone.activities.assistant.viewmodels.AbstractPhoneViewModel
|
||||||
import org.linphone.compatibility.Compatibility
|
import org.linphone.compatibility.Compatibility
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
|
import org.linphone.mediastream.Version
|
||||||
import org.linphone.utils.PermissionHelper
|
import org.linphone.utils.PermissionHelper
|
||||||
import org.linphone.utils.PhoneNumberUtils
|
import org.linphone.utils.PhoneNumberUtils
|
||||||
|
|
||||||
abstract class AbstractPhoneFragment<T : ViewDataBinding> : GenericFragment<T>() {
|
abstract class AbstractPhoneFragment<T : ViewDataBinding> : GenericFragment<T>() {
|
||||||
|
companion object {
|
||||||
|
const val READ_PHONE_STATE_PERMISSION_REQUEST_CODE = 0
|
||||||
|
}
|
||||||
|
|
||||||
abstract val viewModel: AbstractPhoneViewModel
|
abstract val viewModel: AbstractPhoneViewModel
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(
|
override fun onRequestPermissionsResult(
|
||||||
|
@ -39,7 +45,7 @@ abstract class AbstractPhoneFragment<T : ViewDataBinding> : GenericFragment<T>()
|
||||||
permissions: Array<out String>,
|
permissions: Array<out String>,
|
||||||
grantResults: IntArray
|
grantResults: IntArray
|
||||||
) {
|
) {
|
||||||
if (requestCode == 0) {
|
if (requestCode == READ_PHONE_STATE_PERMISSION_REQUEST_CODE) {
|
||||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
Log.i("[Assistant] READ_PHONE_STATE/READ_PHONE_NUMBERS permission granted")
|
Log.i("[Assistant] READ_PHONE_STATE/READ_PHONE_NUMBERS permission granted")
|
||||||
updateFromDeviceInfo()
|
updateFromDeviceInfo()
|
||||||
|
@ -49,11 +55,12 @@ abstract class AbstractPhoneFragment<T : ViewDataBinding> : GenericFragment<T>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fun checkPermission() {
|
@TargetApi(Version.API23_MARSHMALLOW_60)
|
||||||
|
protected fun checkPermissions() {
|
||||||
if (!resources.getBoolean(R.bool.isTablet)) {
|
if (!resources.getBoolean(R.bool.isTablet)) {
|
||||||
if (!PermissionHelper.get().hasReadPhoneStateOrPhoneNumbersPermission()) {
|
if (!PermissionHelper.get().hasReadPhoneStateOrPhoneNumbersPermission()) {
|
||||||
Log.i("[Assistant] Asking for READ_PHONE_STATE/READ_PHONE_NUMBERS permission")
|
Log.i("[Assistant] Asking for READ_PHONE_STATE/READ_PHONE_NUMBERS permission")
|
||||||
Compatibility.requestReadPhoneStateOrNumbersPermission(requireActivity(), 0)
|
Compatibility.requestReadPhoneStateOrNumbersPermission(this, READ_PHONE_STATE_PERMISSION_REQUEST_CODE)
|
||||||
} else {
|
} else {
|
||||||
updateFromDeviceInfo()
|
updateFromDeviceInfo()
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||||
import org.linphone.activities.navigateToEchoCancellerCalibration
|
import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||||
import org.linphone.activities.navigateToPhoneAccountValidation
|
import org.linphone.activities.navigateToPhoneAccountValidation
|
||||||
import org.linphone.databinding.AssistantAccountLoginFragmentBinding
|
import org.linphone.databinding.AssistantAccountLoginFragmentBinding
|
||||||
|
import org.linphone.mediastream.Version
|
||||||
import org.linphone.utils.DialogUtils
|
import org.linphone.utils.DialogUtils
|
||||||
|
|
||||||
class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragmentBinding>() {
|
class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragmentBinding>() {
|
||||||
|
@ -52,7 +53,10 @@ class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragment
|
||||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel = ViewModelProvider(this, AccountLoginViewModelFactory(sharedViewModel.getAccountCreator()))[AccountLoginViewModel::class.java]
|
viewModel = ViewModelProvider(
|
||||||
|
this,
|
||||||
|
AccountLoginViewModelFactory(sharedViewModel.getAccountCreator())
|
||||||
|
)[AccountLoginViewModel::class.java]
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
if (resources.getBoolean(R.bool.isTablet)) {
|
if (resources.getBoolean(R.bool.isTablet)) {
|
||||||
|
@ -75,8 +79,8 @@ class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.goToSmsValidationEvent.observe(
|
viewModel.goToSmsValidationEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
args.putBoolean("IsLogin", true)
|
args.putBoolean("IsLogin", true)
|
||||||
|
@ -84,11 +88,10 @@ class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragment
|
||||||
navigateToPhoneAccountValidation(args)
|
navigateToPhoneAccountValidation(args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.leaveAssistantEvent.observe(
|
viewModel.leaveAssistantEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
coreContext.contactsManager.updateLocalContacts()
|
coreContext.contactsManager.updateLocalContacts()
|
||||||
|
|
||||||
|
@ -99,13 +102,13 @@ class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.invalidCredentialsEvent.observe(
|
viewModel.invalidCredentialsEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
val dialogViewModel = DialogViewModel(getString(R.string.assistant_error_invalid_credentials))
|
val dialogViewModel =
|
||||||
|
DialogViewModel(getString(R.string.assistant_error_invalid_credentials))
|
||||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||||
|
|
||||||
dialogViewModel.showCancelButton {
|
dialogViewModel.showCancelButton {
|
||||||
|
@ -124,17 +127,17 @@ class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragment
|
||||||
dialog.show()
|
dialog.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.onErrorEvent.observe(
|
viewModel.onErrorEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { message ->
|
it.consume { message ->
|
||||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
checkPermission()
|
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
|
||||||
|
checkPermissions()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,10 @@ import org.linphone.databinding.AssistantEchoCancellerCalibrationFragmentBinding
|
||||||
import org.linphone.utils.PermissionHelper
|
import org.linphone.utils.PermissionHelper
|
||||||
|
|
||||||
class EchoCancellerCalibrationFragment : GenericFragment<AssistantEchoCancellerCalibrationFragmentBinding>() {
|
class EchoCancellerCalibrationFragment : GenericFragment<AssistantEchoCancellerCalibrationFragmentBinding>() {
|
||||||
|
companion object {
|
||||||
|
const val RECORD_AUDIO_PERMISSION_REQUEST_CODE = 0
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var viewModel: EchoCancellerCalibrationViewModel
|
private lateinit var viewModel: EchoCancellerCalibrationViewModel
|
||||||
|
|
||||||
override fun getLayoutId(): Int = R.layout.assistant_echo_canceller_calibration_fragment
|
override fun getLayoutId(): Int = R.layout.assistant_echo_canceller_calibration_fragment
|
||||||
|
@ -44,17 +48,16 @@ class EchoCancellerCalibrationFragment : GenericFragment<AssistantEchoCancellerC
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
viewModel.echoCalibrationTerminated.observe(
|
viewModel.echoCalibrationTerminated.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
requireActivity().finish()
|
requireActivity().finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
if (!PermissionHelper.required(requireContext()).hasRecordAudioPermission()) {
|
if (!PermissionHelper.required(requireContext()).hasRecordAudioPermission()) {
|
||||||
Log.i("[Echo Canceller Calibration] Asking for RECORD_AUDIO permission")
|
Log.i("[Echo Canceller Calibration] Asking for RECORD_AUDIO permission")
|
||||||
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 0)
|
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), RECORD_AUDIO_PERMISSION_REQUEST_CODE)
|
||||||
} else {
|
} else {
|
||||||
viewModel.startEchoCancellerCalibration()
|
viewModel.startEchoCancellerCalibration()
|
||||||
}
|
}
|
||||||
|
@ -65,7 +68,9 @@ class EchoCancellerCalibrationFragment : GenericFragment<AssistantEchoCancellerC
|
||||||
permissions: Array<out String>,
|
permissions: Array<out String>,
|
||||||
grantResults: IntArray
|
grantResults: IntArray
|
||||||
) {
|
) {
|
||||||
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
|
if (requestCode == RECORD_AUDIO_PERMISSION_REQUEST_CODE) {
|
||||||
|
val granted =
|
||||||
|
grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||||
if (granted) {
|
if (granted) {
|
||||||
Log.i("[Echo Canceller Calibration] RECORD_AUDIO permission granted")
|
Log.i("[Echo Canceller Calibration] RECORD_AUDIO permission granted")
|
||||||
viewModel.startEchoCancellerCalibration()
|
viewModel.startEchoCancellerCalibration()
|
||||||
|
@ -75,3 +80,4 @@ class EchoCancellerCalibrationFragment : GenericFragment<AssistantEchoCancellerC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -50,21 +50,19 @@ class EmailAccountCreationFragment : GenericFragment<AssistantEmailAccountCreati
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
viewModel.goToEmailValidationEvent.observe(
|
viewModel.goToEmailValidationEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
navigateToEmailAccountValidation()
|
navigateToEmailAccountValidation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.onErrorEvent.observe(
|
viewModel.onErrorEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { message ->
|
it.consume { message ->
|
||||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,8 +49,8 @@ class EmailAccountValidationFragment : GenericFragment<AssistantEmailAccountVali
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
viewModel.leaveAssistantEvent.observe(
|
viewModel.leaveAssistantEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
coreContext.contactsManager.updateLocalContacts()
|
coreContext.contactsManager.updateLocalContacts()
|
||||||
|
|
||||||
|
@ -61,15 +61,13 @@ class EmailAccountValidationFragment : GenericFragment<AssistantEmailAccountVali
|
||||||
navigateToAccountLinking(args)
|
navigateToAccountLinking(args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.onErrorEvent.observe(
|
viewModel.onErrorEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { message ->
|
it.consume { message ->
|
||||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,8 +54,8 @@ class GenericAccountLoginFragment : GenericFragment<AssistantGenericAccountLogin
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
viewModel.leaveAssistantEvent.observe(
|
viewModel.leaveAssistantEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
coreContext.contactsManager.updateLocalContacts()
|
coreContext.contactsManager.updateLocalContacts()
|
||||||
|
|
||||||
|
@ -66,13 +66,13 @@ class GenericAccountLoginFragment : GenericFragment<AssistantGenericAccountLogin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.invalidCredentialsEvent.observe(
|
viewModel.invalidCredentialsEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
val dialogViewModel = DialogViewModel(getString(R.string.assistant_error_invalid_credentials))
|
val dialogViewModel =
|
||||||
|
DialogViewModel(getString(R.string.assistant_error_invalid_credentials))
|
||||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||||
|
|
||||||
dialogViewModel.showCancelButton {
|
dialogViewModel.showCancelButton {
|
||||||
|
@ -91,15 +91,13 @@ class GenericAccountLoginFragment : GenericFragment<AssistantGenericAccountLogin
|
||||||
dialog.show()
|
dialog.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.onErrorEvent.observe(
|
viewModel.onErrorEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { message ->
|
it.consume { message ->
|
||||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.assistant.viewmodels.SharedAssistantViewModel
|
||||||
import org.linphone.activities.navigateToPhoneAccountValidation
|
import org.linphone.activities.navigateToPhoneAccountValidation
|
||||||
import org.linphone.databinding.AssistantPhoneAccountCreationFragmentBinding
|
import org.linphone.databinding.AssistantPhoneAccountCreationFragmentBinding
|
||||||
|
import org.linphone.mediastream.Version
|
||||||
|
|
||||||
class PhoneAccountCreationFragment : AbstractPhoneFragment<AssistantPhoneAccountCreationFragmentBinding>() {
|
class PhoneAccountCreationFragment :
|
||||||
|
AbstractPhoneFragment<AssistantPhoneAccountCreationFragmentBinding>() {
|
||||||
private lateinit var sharedViewModel: SharedAssistantViewModel
|
private lateinit var sharedViewModel: SharedAssistantViewModel
|
||||||
override lateinit var viewModel: PhoneAccountCreationViewModel
|
override lateinit var viewModel: PhoneAccountCreationViewModel
|
||||||
|
|
||||||
|
@ -45,7 +47,10 @@ class PhoneAccountCreationFragment : AbstractPhoneFragment<AssistantPhoneAccount
|
||||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel = ViewModelProvider(this, PhoneAccountCreationViewModelFactory(sharedViewModel.getAccountCreator()))[PhoneAccountCreationViewModel::class.java]
|
viewModel = ViewModelProvider(
|
||||||
|
this,
|
||||||
|
PhoneAccountCreationViewModelFactory(sharedViewModel.getAccountCreator())
|
||||||
|
)[PhoneAccountCreationViewModel::class.java]
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
binding.setInfoClickListener {
|
binding.setInfoClickListener {
|
||||||
|
@ -57,8 +62,8 @@ class PhoneAccountCreationFragment : AbstractPhoneFragment<AssistantPhoneAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.goToSmsValidationEvent.observe(
|
viewModel.goToSmsValidationEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
args.putBoolean("IsCreation", true)
|
args.putBoolean("IsCreation", true)
|
||||||
|
@ -66,17 +71,17 @@ class PhoneAccountCreationFragment : AbstractPhoneFragment<AssistantPhoneAccount
|
||||||
navigateToPhoneAccountValidation(args)
|
navigateToPhoneAccountValidation(args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.onErrorEvent.observe(
|
viewModel.onErrorEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { message ->
|
it.consume { message ->
|
||||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
checkPermission()
|
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
|
||||||
|
checkPermissions()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||||
import org.linphone.activities.navigateToPhoneAccountValidation
|
import org.linphone.activities.navigateToPhoneAccountValidation
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.databinding.AssistantPhoneAccountLinkingFragmentBinding
|
import org.linphone.databinding.AssistantPhoneAccountLinkingFragmentBinding
|
||||||
|
import org.linphone.mediastream.Version
|
||||||
|
|
||||||
class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountLinkingFragmentBinding>() {
|
class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountLinkingFragmentBinding>() {
|
||||||
private lateinit var sharedViewModel: SharedAssistantViewModel
|
private lateinit var sharedViewModel: SharedAssistantViewModel
|
||||||
|
@ -72,8 +73,8 @@ class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountL
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.goToSmsValidationEvent.observe(
|
viewModel.goToSmsValidationEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
args.putBoolean("IsLinking", true)
|
args.putBoolean("IsLinking", true)
|
||||||
|
@ -81,11 +82,10 @@ class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountL
|
||||||
navigateToPhoneAccountValidation(args)
|
navigateToPhoneAccountValidation(args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.leaveAssistantEvent.observe(
|
viewModel.leaveAssistantEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
if (LinphoneApplication.coreContext.core.isEchoCancellerCalibrationRequired) {
|
if (LinphoneApplication.coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||||
navigateToEchoCancellerCalibration()
|
navigateToEchoCancellerCalibration()
|
||||||
|
@ -94,17 +94,17 @@ class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.onErrorEvent.observe(
|
viewModel.onErrorEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { message ->
|
it.consume { message ->
|
||||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
checkPermission()
|
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
|
||||||
|
checkPermissions()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,8 +59,8 @@ class PhoneAccountValidationFragment : GenericFragment<AssistantPhoneAccountVali
|
||||||
viewModel.isLinking.value = arguments?.getBoolean("IsLinking", false)
|
viewModel.isLinking.value = arguments?.getBoolean("IsLinking", false)
|
||||||
|
|
||||||
viewModel.leaveAssistantEvent.observe(
|
viewModel.leaveAssistantEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
when {
|
when {
|
||||||
viewModel.isLogin.value == true || viewModel.isCreation.value == true -> {
|
viewModel.isLogin.value == true || viewModel.isCreation.value == true -> {
|
||||||
|
@ -74,22 +74,23 @@ class PhoneAccountValidationFragment : GenericFragment<AssistantPhoneAccountVali
|
||||||
}
|
}
|
||||||
viewModel.isLinking.value == true -> {
|
viewModel.isLinking.value == true -> {
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
args.putString("Identity", "sip:${viewModel.accountCreator.username}@${viewModel.accountCreator.domain}")
|
args.putString(
|
||||||
|
"Identity",
|
||||||
|
"sip:${viewModel.accountCreator.username}@${viewModel.accountCreator.domain}"
|
||||||
|
)
|
||||||
navigateToAccountSettings(args)
|
navigateToAccountSettings(args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.onErrorEvent.observe(
|
viewModel.onErrorEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { message ->
|
it.consume { message ->
|
||||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
val clipboard = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
val clipboard = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
clipboard.addPrimaryClipChangedListener {
|
clipboard.addPrimaryClipChangedListener {
|
||||||
|
|
|
@ -34,6 +34,10 @@ import org.linphone.databinding.AssistantQrCodeFragmentBinding
|
||||||
import org.linphone.utils.PermissionHelper
|
import org.linphone.utils.PermissionHelper
|
||||||
|
|
||||||
class QrCodeFragment : GenericFragment<AssistantQrCodeFragmentBinding>() {
|
class QrCodeFragment : GenericFragment<AssistantQrCodeFragmentBinding>() {
|
||||||
|
companion object {
|
||||||
|
const val CAMERA_PERMISSION_REQUEST_CODE = 0
|
||||||
|
}
|
||||||
|
|
||||||
private lateinit var sharedViewModel: SharedAssistantViewModel
|
private lateinit var sharedViewModel: SharedAssistantViewModel
|
||||||
private lateinit var viewModel: QrCodeViewModel
|
private lateinit var viewModel: QrCodeViewModel
|
||||||
|
|
||||||
|
@ -52,19 +56,18 @@ class QrCodeFragment : GenericFragment<AssistantQrCodeFragmentBinding>() {
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
viewModel.qrCodeFoundEvent.observe(
|
viewModel.qrCodeFoundEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { url ->
|
it.consume { url ->
|
||||||
sharedViewModel.remoteProvisioningUrl.value = url
|
sharedViewModel.remoteProvisioningUrl.value = url
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
viewModel.setBackCamera()
|
viewModel.setBackCamera()
|
||||||
|
|
||||||
if (!PermissionHelper.required(requireContext()).hasCameraPermission()) {
|
if (!PermissionHelper.required(requireContext()).hasCameraPermission()) {
|
||||||
Log.i("[QR Code] Asking for CAMERA permission")
|
Log.i("[QR Code] Asking for CAMERA permission")
|
||||||
requestPermissions(arrayOf(android.Manifest.permission.CAMERA), 0)
|
requestPermissions(arrayOf(android.Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,14 +75,14 @@ class QrCodeFragment : GenericFragment<AssistantQrCodeFragmentBinding>() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
coreContext.core.nativePreviewWindowId = binding.qrCodeCaptureTexture
|
coreContext.core.nativePreviewWindowId = binding.qrCodeCaptureTexture
|
||||||
coreContext.core.enableQrcodeVideoPreview(true)
|
coreContext.core.isQrcodeVideoPreviewEnabled = true
|
||||||
coreContext.core.enableVideoPreview(true)
|
coreContext.core.isVideoPreviewEnabled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
coreContext.core.nativePreviewWindowId = null
|
coreContext.core.nativePreviewWindowId = null
|
||||||
coreContext.core.enableQrcodeVideoPreview(false)
|
coreContext.core.isQrcodeVideoPreviewEnabled = false
|
||||||
coreContext.core.enableVideoPreview(false)
|
coreContext.core.isVideoPreviewEnabled = false
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
@ -89,7 +92,9 @@ class QrCodeFragment : GenericFragment<AssistantQrCodeFragmentBinding>() {
|
||||||
permissions: Array<out String>,
|
permissions: Array<out String>,
|
||||||
grantResults: IntArray
|
grantResults: IntArray
|
||||||
) {
|
) {
|
||||||
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
|
if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
|
||||||
|
val granted =
|
||||||
|
grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||||
if (granted) {
|
if (granted) {
|
||||||
Log.i("[QR Code] CAMERA permission granted")
|
Log.i("[QR Code] CAMERA permission granted")
|
||||||
coreContext.core.reloadVideoDevices()
|
coreContext.core.reloadVideoDevices()
|
||||||
|
@ -100,3 +105,4 @@ class QrCodeFragment : GenericFragment<AssistantQrCodeFragmentBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -55,8 +55,8 @@ class RemoteProvisioningFragment : GenericFragment<AssistantRemoteProvisioningFr
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.fetchSuccessfulEvent.observe(
|
viewModel.fetchSuccessfulEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { success ->
|
it.consume { success ->
|
||||||
if (success) {
|
if (success) {
|
||||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||||
|
@ -70,7 +70,6 @@ class RemoteProvisioningFragment : GenericFragment<AssistantRemoteProvisioningFr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.urlToFetch.value = sharedViewModel.remoteProvisioningUrl.value ?: coreContext.core.provisioningUri
|
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.assistant.viewmodels.WelcomeViewModel
|
||||||
import org.linphone.activities.navigateToAccountLogin
|
import org.linphone.activities.navigateToAccountLogin
|
||||||
import org.linphone.activities.navigateToEmailAccountCreation
|
import org.linphone.activities.navigateToEmailAccountCreation
|
||||||
import org.linphone.activities.navigateToGenericLogin
|
|
||||||
import org.linphone.activities.navigateToRemoteProvisioning
|
import org.linphone.activities.navigateToRemoteProvisioning
|
||||||
import org.linphone.databinding.AssistantWelcomeFragmentBinding
|
import org.linphone.databinding.AssistantWelcomeFragmentBinding
|
||||||
|
|
||||||
|
@ -65,7 +64,7 @@ class WelcomeFragment : GenericFragment<AssistantWelcomeFragmentBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.setGenericAccountLoginClickListener {
|
binding.setGenericAccountLoginClickListener {
|
||||||
navigateToGenericLogin()
|
navigateToGenericLoginWarning()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.setRemoteProvisioningClickListener {
|
binding.setRemoteProvisioningClickListener {
|
||||||
|
@ -73,11 +72,10 @@ class WelcomeFragment : GenericFragment<AssistantWelcomeFragmentBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.termsAndPrivacyAccepted.observe(
|
viewModel.termsAndPrivacyAccepted.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
if (it) corePreferences.readAndAgreeTermsAndPrivacy = true
|
if (it) corePreferences.readAndAgreeTermsAndPrivacy = true
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
setUpTermsAndPrivacyLinks()
|
setUpTermsAndPrivacyLinks()
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,14 +56,19 @@ abstract class AbstractPhoneViewModel(val accountCreator: AccountCreator) :
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFromPhoneNumberAndOrDialPlan(number: String?, dialPlan: DialPlan?) {
|
fun updateFromPhoneNumberAndOrDialPlan(number: String?, dialPlan: DialPlan?) {
|
||||||
|
val internationalPrefix = "+${dialPlan?.countryCallingCode}"
|
||||||
if (dialPlan != null) {
|
if (dialPlan != null) {
|
||||||
Log.i("[Assistant] Found prefix from dial plan: ${dialPlan.countryCallingCode}")
|
Log.i("[Assistant] Found prefix from dial plan: ${dialPlan.countryCallingCode}")
|
||||||
prefix.value = "+${dialPlan.countryCallingCode}"
|
prefix.value = internationalPrefix
|
||||||
}
|
}
|
||||||
|
|
||||||
if (number != null) {
|
if (number != null) {
|
||||||
Log.i("[Assistant] Found phone number: $number")
|
Log.i("[Assistant] Found phone number: $number")
|
||||||
phoneNumber.value = number!!
|
phoneNumber.value = if (number.startsWith(internationalPrefix)) {
|
||||||
|
number.substring(internationalPrefix.length)
|
||||||
|
} else {
|
||||||
|
number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import org.linphone.R
|
||||||
import org.linphone.core.*
|
import org.linphone.core.*
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.utils.Event
|
import org.linphone.utils.Event
|
||||||
|
import org.linphone.utils.PhoneNumberUtils
|
||||||
|
|
||||||
class AccountLoginViewModelFactory(private val accountCreator: AccountCreator) :
|
class AccountLoginViewModelFactory(private val accountCreator: AccountCreator) :
|
||||||
ViewModelProvider.NewInstanceFactory() {
|
ViewModelProvider.NewInstanceFactory() {
|
||||||
|
@ -220,6 +221,18 @@ class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewM
|
||||||
|
|
||||||
proxyConfig.isPushNotificationAllowed = true
|
proxyConfig.isPushNotificationAllowed = true
|
||||||
|
|
||||||
|
if (proxyConfig.dialPrefix.isNullOrEmpty()) {
|
||||||
|
val dialPlan = PhoneNumberUtils.getDialPlanForCurrentCountry(coreContext.context)
|
||||||
|
if (dialPlan != null) {
|
||||||
|
Log.i("[Assistant] [Account Login] Found dial plan country ${dialPlan.country} with international prefix ${dialPlan.countryCallingCode}")
|
||||||
|
proxyConfig.edit()
|
||||||
|
proxyConfig.dialPrefix = dialPlan.countryCallingCode
|
||||||
|
proxyConfig.done()
|
||||||
|
} else {
|
||||||
|
Log.w("[Assistant] [Account Login] Failed to find dial plan")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Log.i("[Assistant] [Account Login] Proxy config created")
|
Log.i("[Assistant] [Account Login] Proxy config created")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,11 +22,13 @@ package org.linphone.activities.assistant.viewmodels
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.linphone.LinphoneApplication
|
||||||
import org.linphone.core.AccountCreator
|
import org.linphone.core.AccountCreator
|
||||||
import org.linphone.core.AccountCreatorListenerStub
|
import org.linphone.core.AccountCreatorListenerStub
|
||||||
import org.linphone.core.ProxyConfig
|
import org.linphone.core.ProxyConfig
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.utils.Event
|
import org.linphone.utils.Event
|
||||||
|
import org.linphone.utils.PhoneNumberUtils
|
||||||
|
|
||||||
class EmailAccountValidationViewModelFactory(private val accountCreator: AccountCreator) :
|
class EmailAccountValidationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||||
ViewModelProvider.NewInstanceFactory() {
|
ViewModelProvider.NewInstanceFactory() {
|
||||||
|
@ -106,6 +108,18 @@ class EmailAccountValidationViewModel(val accountCreator: AccountCreator) : View
|
||||||
|
|
||||||
proxyConfig.isPushNotificationAllowed = true
|
proxyConfig.isPushNotificationAllowed = true
|
||||||
|
|
||||||
|
if (proxyConfig.dialPrefix.isNullOrEmpty()) {
|
||||||
|
val dialPlan = PhoneNumberUtils.getDialPlanForCurrentCountry(LinphoneApplication.coreContext.context)
|
||||||
|
if (dialPlan != null) {
|
||||||
|
Log.i("[Assistant] [Account Validation] Found dial plan country ${dialPlan.country} with international prefix ${dialPlan.countryCallingCode}")
|
||||||
|
proxyConfig.edit()
|
||||||
|
proxyConfig.dialPrefix = dialPlan.countryCallingCode
|
||||||
|
proxyConfig.done()
|
||||||
|
} else {
|
||||||
|
Log.w("[Assistant] [Account Validation] Failed to find dial plan")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Log.i("[Assistant] [Account Validation] Proxy config created")
|
Log.i("[Assistant] [Account Validation] Proxy config created")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,6 +136,7 @@ class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewMo
|
||||||
Log.e("[Assistant] [Generic Login] Account creator couldn't create proxy config")
|
Log.e("[Assistant] [Generic Login] Account creator couldn't create proxy config")
|
||||||
coreContext.core.removeListener(coreListener)
|
coreContext.core.removeListener(coreListener)
|
||||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||||
|
waitForServerAnswer.value = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -61,8 +61,8 @@ class CallActivity : ProximitySensorActivity() {
|
||||||
sharedViewModel = ViewModelProvider(this)[SharedCallViewModel::class.java]
|
sharedViewModel = ViewModelProvider(this)[SharedCallViewModel::class.java]
|
||||||
|
|
||||||
sharedViewModel.toggleDrawerEvent.observe(
|
sharedViewModel.toggleDrawerEvent.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
if (binding.statsMenu.isDrawerOpen(Gravity.LEFT)) {
|
if (binding.statsMenu.isDrawerOpen(Gravity.LEFT)) {
|
||||||
binding.statsMenu.closeDrawer(binding.sideMenuContent, true)
|
binding.statsMenu.closeDrawer(binding.sideMenuContent, true)
|
||||||
|
@ -71,30 +71,26 @@ class CallActivity : ProximitySensorActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
sharedViewModel.resetHiddenInterfaceTimerInVideoCallEvent.observe(
|
sharedViewModel.resetHiddenInterfaceTimerInVideoCallEvent.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
viewModel.showMomentarily()
|
viewModel.showMomentarily()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.proximitySensorEnabled.observe(
|
viewModel.proximitySensorEnabled.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
enableProximitySensor(it)
|
enableProximitySensor(it)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.videoEnabled.observe(
|
viewModel.videoEnabled.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
updateConstraintSetDependingOnFoldingState()
|
updateConstraintSetDependingOnFoldingState()
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLayoutChanges(foldingFeature: FoldingFeature?) {
|
override fun onLayoutChanges(foldingFeature: FoldingFeature?) {
|
||||||
|
|
|
@ -79,24 +79,22 @@ class IncomingCallActivity : GenericActivity() {
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
viewModel.callEndedEvent.observe(
|
viewModel.callEndedEvent.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
Log.i("[Incoming Call Activity] Call ended, finish activity")
|
Log.i("[Incoming Call Activity] Call ended, finish activity")
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.earlyMediaVideoEnabled.observe(
|
viewModel.earlyMediaVideoEnabled.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
if (it) {
|
if (it) {
|
||||||
Log.i("[Incoming Call Activity] Early media video being received, set native window id")
|
Log.i("[Incoming Call Activity] Early media video being received, set native window id")
|
||||||
coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface
|
coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||||
val keyguardLocked = keyguardManager.isKeyguardLocked
|
val keyguardLocked = keyguardManager.isKeyguardLocked
|
||||||
|
@ -139,7 +137,7 @@ class IncomingCallActivity : GenericActivity() {
|
||||||
permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO)
|
permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewModel.call.currentParams.videoEnabled() && !PermissionHelper.get().hasCameraPermission()) {
|
if (viewModel.call.currentParams.isVideoEnabled && !PermissionHelper.get().hasCameraPermission()) {
|
||||||
Log.i("[Incoming Call Activity] Asking for CAMERA permission")
|
Log.i("[Incoming Call Activity] Asking for CAMERA permission")
|
||||||
permissionsRequiredList.add(Manifest.permission.CAMERA)
|
permissionsRequiredList.add(Manifest.permission.CAMERA)
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,44 +80,48 @@ class OutgoingCallActivity : ProximitySensorActivity() {
|
||||||
binding.controlsViewModel = controlsViewModel
|
binding.controlsViewModel = controlsViewModel
|
||||||
|
|
||||||
viewModel.callEndedEvent.observe(
|
viewModel.callEndedEvent.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
Log.i("[Outgoing Call Activity] Call ended, finish activity")
|
Log.i("[Outgoing Call Activity] Call ended, finish activity")
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.callConnectedEvent.observe(
|
viewModel.callConnectedEvent.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
Log.i("[Outgoing Call Activity] Call connected, finish activity")
|
Log.i("[Outgoing Call Activity] Call connected, finish activity")
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
controlsViewModel.isSpeakerSelected.observe(
|
controlsViewModel.isSpeakerSelected.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
enableProximitySensor(!it)
|
enableProximitySensor(!it)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
controlsViewModel.askPermissionEvent.observe(
|
controlsViewModel.askAudioRecordPermissionEvent.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
|
it.consume { permission ->
|
||||||
|
requestPermissions(arrayOf(permission), 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controlsViewModel.askCameraPermissionEvent.observe(
|
||||||
|
this
|
||||||
|
) {
|
||||||
it.consume { permission ->
|
it.consume { permission ->
|
||||||
requestPermissions(arrayOf(permission), 0)
|
requestPermissions(arrayOf(permission), 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
controlsViewModel.toggleNumpadEvent.observe(
|
controlsViewModel.toggleNumpadEvent.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
it.consume { open ->
|
it.consume { open ->
|
||||||
if (this::numpadAnimator.isInitialized) {
|
if (this::numpadAnimator.isInitialized) {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
@ -128,7 +132,6 @@ class OutgoingCallActivity : ProximitySensorActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
|
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
|
||||||
checkPermissions()
|
checkPermissions()
|
||||||
|
@ -170,7 +173,7 @@ class OutgoingCallActivity : ProximitySensorActivity() {
|
||||||
Log.i("[Outgoing Call Activity] Asking for RECORD_AUDIO permission")
|
Log.i("[Outgoing Call Activity] Asking for RECORD_AUDIO permission")
|
||||||
permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO)
|
permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO)
|
||||||
}
|
}
|
||||||
if (viewModel.call.currentParams.videoEnabled() && !PermissionHelper.get().hasCameraPermission()) {
|
if (viewModel.call.currentParams.isVideoEnabled && !PermissionHelper.get().hasCameraPermission()) {
|
||||||
Log.i("[Outgoing Call Activity] Asking for CAMERA permission")
|
Log.i("[Outgoing Call Activity] Asking for CAMERA permission")
|
||||||
permissionsRequiredList.add(Manifest.permission.CAMERA)
|
permissionsRequiredList.add(Manifest.permission.CAMERA)
|
||||||
}
|
}
|
||||||
|
@ -207,7 +210,8 @@ class OutgoingCallActivity : ProximitySensorActivity() {
|
||||||
for (call in coreContext.core.calls) {
|
for (call in coreContext.core.calls) {
|
||||||
if (call.state == Call.State.OutgoingInit ||
|
if (call.state == Call.State.OutgoingInit ||
|
||||||
call.state == Call.State.OutgoingProgress ||
|
call.state == Call.State.OutgoingProgress ||
|
||||||
call.state == Call.State.OutgoingRinging
|
call.state == Call.State.OutgoingRinging ||
|
||||||
|
call.state == Call.State.OutgoingEarlyMedia
|
||||||
) {
|
) {
|
||||||
return call
|
return call
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ class CallStatisticsData(val call: Call) : GenericContactData(call.remoteAddress
|
||||||
private val listener = object : CoreListenerStub() {
|
private val listener = object : CoreListenerStub() {
|
||||||
override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) {
|
override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) {
|
||||||
if (call == this@CallStatisticsData.call) {
|
if (call == this@CallStatisticsData.call) {
|
||||||
isVideoEnabled.value = call.currentParams.videoEnabled()
|
isVideoEnabled.value = call.currentParams.isVideoEnabled
|
||||||
updateCallStats(stats)
|
updateCallStats(stats)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ class CallStatisticsData(val call: Call) : GenericContactData(call.remoteAddress
|
||||||
|
|
||||||
initCallStats()
|
initCallStats()
|
||||||
|
|
||||||
val videoEnabled = call.currentParams.videoEnabled()
|
val videoEnabled = call.currentParams.isVideoEnabled
|
||||||
isVideoEnabled.value = videoEnabled
|
isVideoEnabled.value = videoEnabled
|
||||||
|
|
||||||
isExpanded.value = coreContext.core.currentCall == call
|
isExpanded.value = coreContext.core.currentCall == call
|
||||||
|
|
|
@ -88,46 +88,43 @@ class ControlsFragment : GenericFragment<CallControlsFragmentBinding>() {
|
||||||
binding.conferenceViewModel = conferenceViewModel
|
binding.conferenceViewModel = conferenceViewModel
|
||||||
|
|
||||||
callsViewModel.currentCallViewModel.observe(
|
callsViewModel.currentCallViewModel.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
binding.activeCallTimer.base =
|
binding.activeCallTimer.base =
|
||||||
SystemClock.elapsedRealtime() - (1000 * it.call.duration) // Linphone timestamps are in seconds
|
SystemClock.elapsedRealtime() - (1000 * it.call.duration) // Linphone timestamps are in seconds
|
||||||
binding.activeCallTimer.start()
|
binding.activeCallTimer.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
callsViewModel.noMoreCallEvent.observe(
|
callsViewModel.noMoreCallEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
requireActivity().finish()
|
requireActivity().finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
callsViewModel.askWriteExternalStoragePermissionEvent.observe(
|
callsViewModel.askWriteExternalStoragePermissionEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
if (!PermissionHelper.get().hasWriteExternalStoragePermission()) {
|
if (!PermissionHelper.get().hasWriteExternalStoragePermission()) {
|
||||||
Log.i("[Controls Fragment] Asking for WRITE_EXTERNAL_STORAGE permission")
|
Log.i("[Controls Fragment] Asking for WRITE_EXTERNAL_STORAGE permission")
|
||||||
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
|
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
callsViewModel.callUpdateEvent.observe(
|
callsViewModel.callUpdateEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { call ->
|
it.consume { call ->
|
||||||
if (call.state == Call.State.StreamsRunning) {
|
if (call.state == Call.State.StreamsRunning) {
|
||||||
dialog?.dismiss()
|
dialog?.dismiss()
|
||||||
} else if (call.state == Call.State.UpdatedByRemote) {
|
} else if (call.state == Call.State.UpdatedByRemote) {
|
||||||
if (coreContext.core.videoCaptureEnabled() || coreContext.core.videoDisplayEnabled()) {
|
if (coreContext.core.isVideoCaptureEnabled || coreContext.core.isVideoDisplayEnabled) {
|
||||||
if (call.currentParams.videoEnabled() != call.remoteParams?.videoEnabled()) {
|
if (call.currentParams.isVideoEnabled != call.remoteParams?.isVideoEnabled) {
|
||||||
showCallVideoUpdateDialog(call)
|
showCallVideoUpdateDialog(call)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -136,11 +133,10 @@ class ControlsFragment : GenericFragment<CallControlsFragmentBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
controlsViewModel.chatClickedEvent.observe(
|
controlsViewModel.chatClickedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
intent.setClass(requireContext(), MainActivity::class.java)
|
intent.setClass(requireContext(), MainActivity::class.java)
|
||||||
|
@ -149,11 +145,10 @@ class ControlsFragment : GenericFragment<CallControlsFragmentBinding>() {
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
controlsViewModel.addCallClickedEvent.observe(
|
controlsViewModel.addCallClickedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
intent.setClass(requireContext(), MainActivity::class.java)
|
intent.setClass(requireContext(), MainActivity::class.java)
|
||||||
|
@ -163,11 +158,10 @@ class ControlsFragment : GenericFragment<CallControlsFragmentBinding>() {
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
controlsViewModel.transferCallClickedEvent.observe(
|
controlsViewModel.transferCallClickedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
intent.setClass(requireContext(), MainActivity::class.java)
|
intent.setClass(requireContext(), MainActivity::class.java)
|
||||||
|
@ -177,21 +171,28 @@ class ControlsFragment : GenericFragment<CallControlsFragmentBinding>() {
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
controlsViewModel.askPermissionEvent.observe(
|
controlsViewModel.askAudioRecordPermissionEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { permission ->
|
it.consume { permission ->
|
||||||
Log.i("[Controls Fragment] Asking for $permission permission")
|
Log.i("[Controls Fragment] Asking for $permission permission")
|
||||||
requestPermissions(arrayOf(permission), 0)
|
requestPermissions(arrayOf(permission), 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
controlsViewModel.askCameraPermissionEvent.observe(
|
||||||
|
viewLifecycleOwner
|
||||||
|
) {
|
||||||
|
it.consume { permission ->
|
||||||
|
Log.i("[Controls Fragment] Asking for $permission permission")
|
||||||
|
requestPermissions(arrayOf(permission), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
controlsViewModel.toggleNumpadEvent.observe(
|
controlsViewModel.toggleNumpadEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { open ->
|
it.consume { open ->
|
||||||
if (this::numpadAnimator.isInitialized) {
|
if (this::numpadAnimator.isInitialized) {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
@ -202,16 +203,14 @@ class ControlsFragment : GenericFragment<CallControlsFragmentBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
controlsViewModel.somethingClickedEvent.observe(
|
controlsViewModel.somethingClickedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
sharedViewModel.resetHiddenInterfaceTimerInVideoCallEvent.value = Event(true)
|
sharedViewModel.resetHiddenInterfaceTimerInVideoCallEvent.value = Event(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
|
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
|
||||||
checkPermissions()
|
checkPermissions()
|
||||||
|
@ -251,7 +250,13 @@ class ControlsFragment : GenericFragment<CallControlsFragmentBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (requestCode == 1 && grantResults.isNotEmpty() && grantResults[0] == PERMISSION_GRANTED) {
|
} else if (requestCode == 1) {
|
||||||
|
if (grantResults.isNotEmpty() && grantResults[0] == PERMISSION_GRANTED) {
|
||||||
|
Log.i("[Controls Fragment] CAMERA permission has been granted")
|
||||||
|
coreContext.core.reloadVideoDevices()
|
||||||
|
controlsViewModel.toggleVideo()
|
||||||
|
}
|
||||||
|
} else if (requestCode == 2 && grantResults.isNotEmpty() && grantResults[0] == PERMISSION_GRANTED) {
|
||||||
callsViewModel.takeScreenshot()
|
callsViewModel.takeScreenshot()
|
||||||
}
|
}
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
|
|
@ -64,15 +64,14 @@ class StatusFragment : GenericFragment<CallStatusFragmentBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.showZrtpDialogEvent.observe(
|
viewModel.showZrtpDialogEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { call ->
|
it.consume { call ->
|
||||||
if (call.state == Call.State.Connected || call.state == Call.State.StreamsRunning) {
|
if (call.state == Call.State.Connected || call.state == Call.State.StreamsRunning) {
|
||||||
showZrtpDialog(call)
|
showZrtpDialog(call)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -114,8 +113,8 @@ class StatusFragment : GenericFragment<CallStatusFragmentBinding>() {
|
||||||
|
|
||||||
val viewModel = DialogViewModel(getString(R.string.zrtp_dialog_message), getString(R.string.zrtp_dialog_title))
|
val viewModel = DialogViewModel(getString(R.string.zrtp_dialog_message), getString(R.string.zrtp_dialog_title))
|
||||||
viewModel.showZrtp = true
|
viewModel.showZrtp = true
|
||||||
viewModel.zrtpReadSas = toRead.toUpperCase(Locale.getDefault())
|
viewModel.zrtpReadSas = toRead.uppercase(Locale.getDefault())
|
||||||
viewModel.zrtpListenSas = toListen.toUpperCase(Locale.getDefault())
|
viewModel.zrtpListenSas = toListen.uppercase(Locale.getDefault())
|
||||||
viewModel.showIcon = true
|
viewModel.showIcon = true
|
||||||
viewModel.iconResource = R.drawable.security_2_indicator
|
viewModel.iconResource = R.drawable.security_2_indicator
|
||||||
|
|
||||||
|
|
|
@ -114,6 +114,7 @@ open class CallViewModel(val call: Call) : GenericContactViewModel(call.remoteAd
|
||||||
call.addListener(listener)
|
call.addListener(listener)
|
||||||
|
|
||||||
isPaused.value = call.state == Call.State.Paused
|
isPaused.value = call.state == Call.State.Paused
|
||||||
|
isOutgoingEarlyMedia.value = call.state == Call.State.OutgoingEarlyMedia
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
@ -138,7 +139,7 @@ open class CallViewModel(val call: Call) : GenericContactViewModel(call.remoteAd
|
||||||
}
|
}
|
||||||
|
|
||||||
fun takeScreenshot() {
|
fun takeScreenshot() {
|
||||||
if (call.currentParams.videoEnabled()) {
|
if (call.currentParams.isVideoEnabled) {
|
||||||
val fileName = System.currentTimeMillis().toString() + ".jpeg"
|
val fileName = System.currentTimeMillis().toString() + ".jpeg"
|
||||||
call.takeVideoSnapshot(FileUtils.getFileStoragePath(fileName).absolutePath)
|
call.takeVideoSnapshot(FileUtils.getFileStoragePath(fileName).absolutePath)
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,11 +75,11 @@ class CallsViewModel : ViewModel() {
|
||||||
} else if (call.state == Call.State.UpdatedByRemote) {
|
} else if (call.state == Call.State.UpdatedByRemote) {
|
||||||
// If the correspondent asks to turn on video while audio call,
|
// If the correspondent asks to turn on video while audio call,
|
||||||
// defer update until user has chosen whether to accept it or not
|
// defer update until user has chosen whether to accept it or not
|
||||||
val remoteVideo = call.remoteParams?.videoEnabled() ?: false
|
val remoteVideo = call.remoteParams?.isVideoEnabled ?: false
|
||||||
val localVideo = call.currentParams.videoEnabled()
|
val localVideo = call.currentParams.isVideoEnabled
|
||||||
val autoAccept = call.core.videoActivationPolicy.automaticallyAccept
|
val autoAccept = call.core.videoActivationPolicy.automaticallyAccept
|
||||||
if (remoteVideo && !localVideo && !autoAccept) {
|
if (remoteVideo && !localVideo && !autoAccept) {
|
||||||
if (coreContext.core.videoCaptureEnabled() || coreContext.core.videoDisplayEnabled()) {
|
if (coreContext.core.isVideoCaptureEnabled || coreContext.core.isVideoDisplayEnabled) {
|
||||||
call.deferUpdate()
|
call.deferUpdate()
|
||||||
callUpdateEvent.value = Event(call)
|
callUpdateEvent.value = Event(call)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -87,7 +87,11 @@ class ControlsViewModel : ViewModel() {
|
||||||
MutableLiveData<Event<Boolean>>()
|
MutableLiveData<Event<Boolean>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
val askPermissionEvent: MutableLiveData<Event<String>> by lazy {
|
val askAudioRecordPermissionEvent: MutableLiveData<Event<String>> by lazy {
|
||||||
|
MutableLiveData<Event<String>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val askCameraPermissionEvent: MutableLiveData<Event<String>> by lazy {
|
||||||
MutableLiveData<Event<String>>()
|
MutableLiveData<Event<String>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,7 +180,7 @@ class ControlsViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coreContext.isVideoCallOrConferenceActive() && !PermissionHelper.get().hasCameraPermission()) {
|
if (coreContext.isVideoCallOrConferenceActive() && !PermissionHelper.get().hasCameraPermission()) {
|
||||||
askPermissionEvent.value = Event(Manifest.permission.CAMERA)
|
askCameraPermissionEvent.value = Event(Manifest.permission.CAMERA)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUI()
|
updateUI()
|
||||||
|
@ -244,13 +248,13 @@ class ControlsViewModel : ViewModel() {
|
||||||
|
|
||||||
fun toggleMuteMicrophone() {
|
fun toggleMuteMicrophone() {
|
||||||
if (!PermissionHelper.get().hasRecordAudioPermission()) {
|
if (!PermissionHelper.get().hasRecordAudioPermission()) {
|
||||||
askPermissionEvent.value = Event(Manifest.permission.RECORD_AUDIO)
|
askAudioRecordPermissionEvent.value = Event(Manifest.permission.RECORD_AUDIO)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
somethingClickedEvent.value = Event(true)
|
somethingClickedEvent.value = Event(true)
|
||||||
val micEnabled = coreContext.core.micEnabled()
|
val micEnabled = coreContext.core.isMicEnabled
|
||||||
coreContext.core.enableMic(!micEnabled)
|
coreContext.core.isMicEnabled = !micEnabled
|
||||||
updateMuteMicState()
|
updateMuteMicState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,7 +283,7 @@ class ControlsViewModel : ViewModel() {
|
||||||
|
|
||||||
fun toggleVideo() {
|
fun toggleVideo() {
|
||||||
if (!PermissionHelper.get().hasCameraPermission()) {
|
if (!PermissionHelper.get().hasCameraPermission()) {
|
||||||
askPermissionEvent.value = Event(Manifest.permission.CAMERA)
|
askCameraPermissionEvent.value = Event(Manifest.permission.CAMERA)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,7 +304,7 @@ class ControlsViewModel : ViewModel() {
|
||||||
|
|
||||||
isVideoUpdateInProgress.value = true
|
isVideoUpdateInProgress.value = true
|
||||||
val params = core.createCallParams(currentCall)
|
val params = core.createCallParams(currentCall)
|
||||||
params?.enableVideo(!currentCall.currentParams.videoEnabled())
|
params?.isVideoEnabled = !currentCall.currentParams.isVideoEnabled
|
||||||
currentCall.update(params)
|
currentCall.update(params)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -338,14 +342,16 @@ class ControlsViewModel : ViewModel() {
|
||||||
val currentCall = core.currentCall
|
val currentCall = core.currentCall
|
||||||
val conference = core.conference
|
val conference = core.conference
|
||||||
|
|
||||||
if (currentCall != null) {
|
when {
|
||||||
|
currentCall != null -> {
|
||||||
if (currentCall.isRecording) {
|
if (currentCall.isRecording) {
|
||||||
currentCall.stopRecording()
|
currentCall.stopRecording()
|
||||||
} else {
|
} else {
|
||||||
currentCall.startRecording()
|
currentCall.startRecording()
|
||||||
}
|
}
|
||||||
isRecording.value = currentCall.isRecording
|
isRecording.value = currentCall.isRecording
|
||||||
} else if (conference != null) {
|
}
|
||||||
|
conference != null -> {
|
||||||
val path = LinphoneUtils.getRecordingFilePathForConference()
|
val path = LinphoneUtils.getRecordingFilePathForConference()
|
||||||
if (conference.isRecording) {
|
if (conference.isRecording) {
|
||||||
conference.stopRecording()
|
conference.stopRecording()
|
||||||
|
@ -353,9 +359,11 @@ class ControlsViewModel : ViewModel() {
|
||||||
conference.startRecording(path)
|
conference.startRecording(path)
|
||||||
}
|
}
|
||||||
isRecording.value = conference.isRecording
|
isRecording.value = conference.isRecording
|
||||||
} else {
|
}
|
||||||
|
else -> {
|
||||||
isRecording.value = false
|
isRecording.value = false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (closeMenu) toggleOptionsMenu()
|
if (closeMenu) toggleOptionsMenu()
|
||||||
}
|
}
|
||||||
|
@ -378,7 +386,7 @@ class ControlsViewModel : ViewModel() {
|
||||||
somethingClickedEvent.value = Event(true)
|
somethingClickedEvent.value = Event(true)
|
||||||
|
|
||||||
val core = coreContext.core
|
val core = coreContext.core
|
||||||
val currentCallVideoEnabled = core.currentCall?.currentParams?.videoEnabled() ?: false
|
val currentCallVideoEnabled = core.currentCall?.currentParams?.isVideoEnabled ?: false
|
||||||
|
|
||||||
val params = core.createConferenceParams()
|
val params = core.createConferenceParams()
|
||||||
params.isVideoEnabled = currentCallVideoEnabled
|
params.isVideoEnabled = currentCallVideoEnabled
|
||||||
|
@ -411,7 +419,7 @@ class ControlsViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateMuteMicState() {
|
fun updateMuteMicState() {
|
||||||
isMicrophoneMuted.value = !PermissionHelper.get().hasRecordAudioPermission() || !coreContext.core.micEnabled()
|
isMicrophoneMuted.value = !PermissionHelper.get().hasRecordAudioPermission() || !coreContext.core.isMicEnabled
|
||||||
isMuteMicrophoneEnabled.value = coreContext.core.currentCall != null || coreContext.core.conference?.isIn == true
|
isMuteMicrophoneEnabled.value = coreContext.core.currentCall != null || coreContext.core.conference?.isIn == true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -457,7 +465,7 @@ class ControlsViewModel : ViewModel() {
|
||||||
private fun updateVideoAvailable() {
|
private fun updateVideoAvailable() {
|
||||||
val core = coreContext.core
|
val core = coreContext.core
|
||||||
val currentCall = core.currentCall
|
val currentCall = core.currentCall
|
||||||
isVideoAvailable.value = (core.videoCaptureEnabled() || core.videoPreviewEnabled()) &&
|
isVideoAvailable.value = (core.isVideoCaptureEnabled || core.isVideoPreviewEnabled) &&
|
||||||
(
|
(
|
||||||
(currentCall != null && !currentCall.mediaInProgress()) ||
|
(currentCall != null && !currentCall.mediaInProgress()) ||
|
||||||
core.conference?.isIn == true
|
core.conference?.isIn == true
|
||||||
|
|
|
@ -60,10 +60,10 @@ class IncomingCallViewModel(call: Call) : CallViewModel(call) {
|
||||||
coreContext.core.addListener(listener)
|
coreContext.core.addListener(listener)
|
||||||
|
|
||||||
screenLocked.value = false
|
screenLocked.value = false
|
||||||
inviteWithVideo.value = call.remoteParams?.videoEnabled() == true && coreContext.core.videoActivationPolicy.automaticallyAccept
|
inviteWithVideo.value = call.remoteParams?.isVideoEnabled == true && coreContext.core.videoActivationPolicy.automaticallyAccept
|
||||||
earlyMediaVideoEnabled.value = corePreferences.acceptEarlyMedia &&
|
earlyMediaVideoEnabled.value = corePreferences.acceptEarlyMedia &&
|
||||||
call.state == Call.State.IncomingEarlyMedia &&
|
call.state == Call.State.IncomingEarlyMedia &&
|
||||||
call.currentParams.videoEnabled()
|
call.currentParams.isVideoEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
|
|
@ -37,6 +37,8 @@ import org.linphone.activities.main.chat.adapters.ChatMessagesListAdapter
|
||||||
import org.linphone.activities.main.chat.viewmodels.*
|
import org.linphone.activities.main.chat.viewmodels.*
|
||||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||||
import org.linphone.core.ChatRoom
|
import org.linphone.core.ChatRoom
|
||||||
|
import org.linphone.core.ChatRoomListenerStub
|
||||||
|
import org.linphone.core.EventLog
|
||||||
import org.linphone.core.Factory
|
import org.linphone.core.Factory
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.databinding.ChatBubbleActivityBinding
|
import org.linphone.databinding.ChatBubbleActivityBinding
|
||||||
|
@ -58,6 +60,12 @@ class ChatBubbleActivity : GenericActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val listener = object : ChatRoomListenerStub() {
|
||||||
|
override fun onChatMessageReceived(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||||
|
chatRoom.markAsRead()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
@ -86,11 +94,6 @@ class ChatBubbleActivity : GenericActivity() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workaround for the removed notification when a chat room is marked as read
|
|
||||||
coreContext.notificationsManager.dismissNotificationUponReadChatRoom = false
|
|
||||||
chatRoom.markAsRead()
|
|
||||||
coreContext.notificationsManager.dismissNotificationUponReadChatRoom = true
|
|
||||||
|
|
||||||
viewModel = ViewModelProvider(
|
viewModel = ViewModelProvider(
|
||||||
this,
|
this,
|
||||||
ChatRoomViewModelFactory(chatRoom)
|
ChatRoomViewModelFactory(chatRoom)
|
||||||
|
@ -119,38 +122,40 @@ class ChatBubbleActivity : GenericActivity() {
|
||||||
adapter.disableContextMenu()
|
adapter.disableContextMenu()
|
||||||
|
|
||||||
adapter.openContentEvent.observe(
|
adapter.openContentEvent.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
it.consume { content ->
|
it.consume { content ->
|
||||||
if (content.isFileEncrypted) {
|
if (content.isFileEncrypted) {
|
||||||
Toast.makeText(this, R.string.chat_bubble_cant_open_enrypted_file, Toast.LENGTH_LONG).show()
|
Toast.makeText(
|
||||||
|
this,
|
||||||
|
R.string.chat_bubble_cant_open_enrypted_file,
|
||||||
|
Toast.LENGTH_LONG
|
||||||
|
).show()
|
||||||
} else {
|
} else {
|
||||||
FileUtils.openFileInThirdPartyApp(this, content.filePath.orEmpty(), true)
|
FileUtils.openFileInThirdPartyApp(this, content.filePath.orEmpty(), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
val layoutManager = LinearLayoutManager(this)
|
val layoutManager = LinearLayoutManager(this)
|
||||||
layoutManager.stackFromEnd = true
|
layoutManager.stackFromEnd = true
|
||||||
binding.chatMessagesList.layoutManager = layoutManager
|
binding.chatMessagesList.layoutManager = layoutManager
|
||||||
|
|
||||||
listViewModel.events.observe(
|
listViewModel.events.observe(
|
||||||
this,
|
this
|
||||||
{ events ->
|
) { events ->
|
||||||
adapter.submitList(events)
|
adapter.submitList(events)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
chatSendingViewModel.textToSend.observe(
|
chatSendingViewModel.textToSend.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
chatSendingViewModel.onTextToSendChanged(it)
|
chatSendingViewModel.onTextToSendChanged(it)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
binding.setOpenAppClickListener {
|
binding.setOpenAppClickListener {
|
||||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
||||||
|
coreContext.notificationsManager.changeDismissNotificationUponReadForChatRoom(viewModel.chatRoom, false)
|
||||||
|
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
intent.putExtra("RemoteSipUri", remoteSipUri)
|
intent.putExtra("RemoteSipUri", remoteSipUri)
|
||||||
|
@ -173,6 +178,12 @@ class ChatBubbleActivity : GenericActivity() {
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
|
viewModel.chatRoom.addListener(listener)
|
||||||
|
|
||||||
|
// Workaround for the removed notification when a chat room is marked as read
|
||||||
|
coreContext.notificationsManager.changeDismissNotificationUponReadForChatRoom(viewModel.chatRoom, true)
|
||||||
|
viewModel.chatRoom.markAsRead()
|
||||||
|
|
||||||
val peerAddress = viewModel.chatRoom.peerAddress.asStringUriOnly()
|
val peerAddress = viewModel.chatRoom.peerAddress.asStringUriOnly()
|
||||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = peerAddress
|
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = peerAddress
|
||||||
coreContext.notificationsManager.resetChatNotificationCounterForSipUri(peerAddress)
|
coreContext.notificationsManager.resetChatNotificationCounterForSipUri(peerAddress)
|
||||||
|
@ -185,7 +196,10 @@ class ChatBubbleActivity : GenericActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
viewModel.chatRoom.removeListener(listener)
|
||||||
|
|
||||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
||||||
|
coreContext.notificationsManager.changeDismissNotificationUponReadForChatRoom(viewModel.chatRoom, false)
|
||||||
|
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.doOnAttach
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.fragment.app.FragmentContainerView
|
import androidx.fragment.app.FragmentContainerView
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
@ -48,8 +50,7 @@ import kotlinx.coroutines.*
|
||||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||||
import org.linphone.R
|
import org.linphone.R
|
||||||
import org.linphone.activities.GenericActivity
|
import org.linphone.activities.*
|
||||||
import org.linphone.activities.SnackBarActivity
|
|
||||||
import org.linphone.activities.assistant.AssistantActivity
|
import org.linphone.activities.assistant.AssistantActivity
|
||||||
import org.linphone.activities.main.viewmodels.CallOverlayViewModel
|
import org.linphone.activities.main.viewmodels.CallOverlayViewModel
|
||||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||||
|
@ -112,6 +113,8 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val splashScreen = installSplashScreen()
|
||||||
|
|
||||||
binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
|
binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
|
||||||
binding.lifecycleOwner = this
|
binding.lifecycleOwner = this
|
||||||
|
|
||||||
|
@ -122,8 +125,8 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
binding.callOverlayViewModel = callOverlayViewModel
|
binding.callOverlayViewModel = callOverlayViewModel
|
||||||
|
|
||||||
sharedViewModel.toggleDrawerEvent.observe(
|
sharedViewModel.toggleDrawerEvent.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
if (binding.sideMenu.isDrawerOpen(Gravity.LEFT)) {
|
if (binding.sideMenu.isDrawerOpen(Gravity.LEFT)) {
|
||||||
binding.sideMenu.closeDrawer(binding.sideMenuContent, true)
|
binding.sideMenu.closeDrawer(binding.sideMenuContent, true)
|
||||||
|
@ -132,20 +135,17 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
coreContext.callErrorMessageResourceId.observe(
|
coreContext.callErrorMessageResourceId.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
it.consume { message ->
|
it.consume { message ->
|
||||||
showSnackBar(message)
|
showSnackBar(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
if (coreContext.core.accountList.isEmpty()) {
|
if (coreContext.core.accountList.isEmpty()) {
|
||||||
if (corePreferences.firstStart) {
|
if (corePreferences.firstStart) {
|
||||||
corePreferences.firstStart = false
|
|
||||||
startActivity(Intent(this, AssistantActivity::class.java))
|
startActivity(Intent(this, AssistantActivity::class.java))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -153,7 +153,14 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
tabsFragment = findViewById(R.id.tabs_fragment)
|
tabsFragment = findViewById(R.id.tabs_fragment)
|
||||||
statusFragment = findViewById(R.id.status_fragment)
|
statusFragment = findViewById(R.id.status_fragment)
|
||||||
|
|
||||||
initOverlay()
|
binding.root.doOnAttach {
|
||||||
|
Log.i("[Main Activity] Report UI has been fully drawn (TTFD)")
|
||||||
|
try {
|
||||||
|
reportFullyDrawn()
|
||||||
|
} catch (se: SecurityException) {
|
||||||
|
Log.e("[Main Activity] Security exception when doing reportFullyDrawn(): $se")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
@ -176,6 +183,16 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
Snackbar.make(findViewById(R.id.coordinator), resourceId, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(findViewById(R.id.coordinator), resourceId, Snackbar.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun showSnackBar(resourceId: Int, action: Int, listener: () -> Unit) {
|
||||||
|
Snackbar
|
||||||
|
.make(findViewById(R.id.coordinator), resourceId, Snackbar.LENGTH_LONG)
|
||||||
|
.setAction(action) {
|
||||||
|
Log.i("[Snack Bar] Action listener triggered")
|
||||||
|
listener()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
override fun showSnackBar(message: String) {
|
override fun showSnackBar(message: String) {
|
||||||
Snackbar.make(findViewById(R.id.coordinator), message, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(findViewById(R.id.coordinator), message, Snackbar.LENGTH_LONG).show()
|
||||||
}
|
}
|
||||||
|
@ -195,6 +212,8 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
updateTabsFragmentVisibility()
|
updateTabsFragmentVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initOverlay()
|
||||||
|
|
||||||
if (intent != null) handleIntentParams(intent)
|
if (intent != null) handleIntentParams(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,7 +228,7 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
destination: NavDestination,
|
destination: NavDestination,
|
||||||
arguments: Bundle?
|
arguments: Bundle?
|
||||||
) {
|
) {
|
||||||
currentFocus?.hideKeyboard()
|
hideKeyboard()
|
||||||
if (statusFragment.visibility == View.GONE) {
|
if (statusFragment.visibility == View.GONE) {
|
||||||
statusFragment.visibility = View.VISIBLE
|
statusFragment.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
|
@ -222,6 +241,10 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
updateTabsFragmentVisibility()
|
updateTabsFragmentVisibility()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun hideKeyboard() {
|
||||||
|
currentFocus?.hideKeyboard()
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateTabsFragmentVisibility() {
|
private fun updateTabsFragmentVisibility() {
|
||||||
tabsFragment.visibility = if (tabsFragmentVisible1 && tabsFragmentVisible2) View.VISIBLE else View.GONE
|
tabsFragment.visibility = if (tabsFragmentVisible1 && tabsFragmentVisible2) View.VISIBLE else View.GONE
|
||||||
}
|
}
|
||||||
|
@ -253,9 +276,8 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
val contactId = coreContext.contactsManager.getAndroidContactIdFromUri(uri)
|
val contactId = coreContext.contactsManager.getAndroidContactIdFromUri(uri)
|
||||||
if (contactId != null) {
|
if (contactId != null) {
|
||||||
val deepLink = "linphone-android://contact/view/$contactId"
|
Log.i("[Main Activity] Found contact URI parameter in intent: $uri")
|
||||||
Log.i("[Main Activity] Found contact URI parameter in intent: $uri, starting deep link: $deepLink")
|
navigateToContact(contactId)
|
||||||
findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -282,9 +304,8 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
when {
|
when {
|
||||||
intent.hasExtra("ContactId") -> {
|
intent.hasExtra("ContactId") -> {
|
||||||
val id = intent.getStringExtra("ContactId")
|
val id = intent.getStringExtra("ContactId")
|
||||||
val deepLink = "linphone-android://contact/view/$id"
|
Log.i("[Main Activity] Found contact ID in extras: $id")
|
||||||
Log.i("[Main Activity] Found contact id parameter in intent: $id, starting deep link: $deepLink")
|
navigateToContact(id)
|
||||||
findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
|
|
||||||
}
|
}
|
||||||
intent.hasExtra("Chat") -> {
|
intent.hasExtra("Chat") -> {
|
||||||
if (corePreferences.disableChat) return
|
if (corePreferences.disableChat) return
|
||||||
|
@ -293,10 +314,10 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
val peerAddress = intent.getStringExtra("RemoteSipUri")
|
val peerAddress = intent.getStringExtra("RemoteSipUri")
|
||||||
val localAddress = intent.getStringExtra("LocalSipUri")
|
val localAddress = intent.getStringExtra("LocalSipUri")
|
||||||
Log.i("[Main Activity] Found chat room intent extra: local SIP URI=[$localAddress], peer SIP URI=[$peerAddress]")
|
Log.i("[Main Activity] Found chat room intent extra: local SIP URI=[$localAddress], peer SIP URI=[$peerAddress]")
|
||||||
findNavController(R.id.nav_host_fragment).navigate(Uri.parse("linphone-android://chat-room/$localAddress/$peerAddress"))
|
navigateToChatRoom(localAddress, peerAddress)
|
||||||
} else {
|
} else {
|
||||||
Log.i("[Main Activity] Found chat intent extra, go to chat rooms list")
|
Log.i("[Main Activity] Found chat intent extra, go to chat rooms list")
|
||||||
findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_masterChatRoomsFragment)
|
navigateToChatRooms()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
intent.hasExtra("Dialer") -> {
|
intent.hasExtra("Dialer") -> {
|
||||||
|
@ -427,12 +448,11 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
addressToIM = addressToIM.substring("mmsto:".length)
|
addressToIM = addressToIM.substring("mmsto:".length)
|
||||||
}
|
}
|
||||||
|
|
||||||
val peerAddress = coreContext.core.interpretUrl(addressToIM)?.asStringUriOnly()
|
|
||||||
val localAddress =
|
val localAddress =
|
||||||
coreContext.core.defaultAccount?.params?.identityAddress?.asStringUriOnly()
|
coreContext.core.defaultAccount?.params?.identityAddress?.asStringUriOnly()
|
||||||
val deepLink = "linphone-android://chat-room/$localAddress/$peerAddress"
|
val peerAddress = coreContext.core.interpretUrl(addressToIM)?.asStringUriOnly()
|
||||||
Log.i("[Main Activity] Starting deep link: $deepLink")
|
Log.i("[Main Activity] Navigating to chat room with local [$localAddress] and peer [$peerAddress] addresses")
|
||||||
findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
|
navigateToChatRoom(localAddress, peerAddress)
|
||||||
} else {
|
} else {
|
||||||
val shortcutId = intent.getStringExtra("android.intent.extra.shortcut.ID") // Intent.EXTRA_SHORTCUT_ID
|
val shortcutId = intent.getStringExtra("android.intent.extra.shortcut.ID") // Intent.EXTRA_SHORTCUT_ID
|
||||||
if (shortcutId != null) {
|
if (shortcutId != null) {
|
||||||
|
@ -440,7 +460,7 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
handleLocusOrShortcut(shortcutId)
|
handleLocusOrShortcut(shortcutId)
|
||||||
} else {
|
} else {
|
||||||
Log.i("[Main Activity] Going into chat rooms list")
|
Log.i("[Main Activity] Going into chat rooms list")
|
||||||
findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_masterChatRoomsFragment)
|
navigateToChatRooms()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -450,11 +470,11 @@ class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestin
|
||||||
if (split.size == 2) {
|
if (split.size == 2) {
|
||||||
val localAddress = split[0]
|
val localAddress = split[0]
|
||||||
val peerAddress = split[1]
|
val peerAddress = split[1]
|
||||||
val deepLink = "linphone-android://chat-room/$localAddress/$peerAddress"
|
Log.i("[Main Activity] Navigating to chat room with local [$localAddress] and peer [$peerAddress] addresses, computed from shortcut/locus id")
|
||||||
findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
|
navigateToChatRoom(localAddress, peerAddress)
|
||||||
} else {
|
} else {
|
||||||
Log.e("[Main Activity] Failed to parse shortcut/locus id: $id")
|
Log.e("[Main Activity] Failed to parse shortcut/locus id: $id, going to chat rooms list")
|
||||||
findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_masterChatRoomsFragment)
|
navigateToChatRooms()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,5 +58,13 @@ class AboutFragment : SecureFragment<AboutFragmentBinding>() {
|
||||||
)
|
)
|
||||||
startActivity(browserIntent)
|
startActivity(browserIntent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.setWeblateClickListener {
|
||||||
|
val browserIntent = Intent(
|
||||||
|
Intent.ACTION_VIEW,
|
||||||
|
Uri.parse(getString(R.string.about_weblate_link))
|
||||||
|
)
|
||||||
|
startActivity(browserIntent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ internal abstract class ChatScrollListener(private val mLayoutManager: LinearLay
|
||||||
// True if we are still waiting for the last set of data to load.
|
// True if we are still waiting for the last set of data to load.
|
||||||
private var loading = true
|
private var loading = true
|
||||||
|
|
||||||
var userHasScrolledUp: Boolean = false
|
private var userHasScrolledUp: Boolean = false
|
||||||
|
|
||||||
// This happens many times a second during a scroll, so be wary of the code you place here.
|
// This happens many times a second during a scroll, so be wary of the code you place here.
|
||||||
// We are given a few useful parameters to help us work out if we need to load some more data,
|
// We are given a few useful parameters to help us work out if we need to load some more data,
|
||||||
|
|
|
@ -86,6 +86,10 @@ class ChatMessagesListAdapter(
|
||||||
MutableLiveData<Event<Content>>()
|
MutableLiveData<Event<Content>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val sipUriClickedEvent: MutableLiveData<Event<String>> by lazy {
|
||||||
|
MutableLiveData<Event<String>>()
|
||||||
|
}
|
||||||
|
|
||||||
val scrollToChatMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
val scrollToChatMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||||
MutableLiveData<Event<ChatMessage>>()
|
MutableLiveData<Event<ChatMessage>>()
|
||||||
}
|
}
|
||||||
|
@ -94,6 +98,10 @@ class ChatMessagesListAdapter(
|
||||||
override fun onContentClicked(content: Content) {
|
override fun onContentClicked(content: Content) {
|
||||||
openContentEvent.value = Event(content)
|
openContentEvent.value = Event(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSipAddressClicked(sipUri: String) {
|
||||||
|
sipUriClickedEvent.value = Event(sipUri)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var contextMenuDisabled: Boolean = false
|
private var contextMenuDisabled: Boolean = false
|
||||||
|
@ -211,15 +219,14 @@ class ChatMessagesListAdapter(
|
||||||
// This is for item selection through ListTopBarFragment
|
// This is for item selection through ListTopBarFragment
|
||||||
selectionListViewModel = selectionViewModel
|
selectionListViewModel = selectionViewModel
|
||||||
selectionViewModel.isEditionEnabled.observe(
|
selectionViewModel.isEditionEnabled.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
position = adapterPosition
|
position = bindingAdapterPosition
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
setClickListener {
|
setClickListener {
|
||||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||||
selectionViewModel.onToggleSelect(adapterPosition)
|
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,8 +241,8 @@ class ChatMessagesListAdapter(
|
||||||
var hasPrevious = false
|
var hasPrevious = false
|
||||||
var hasNext = false
|
var hasNext = false
|
||||||
|
|
||||||
if (adapterPosition > 0) {
|
if (bindingAdapterPosition > 0) {
|
||||||
val previousItem = getItem(adapterPosition - 1)
|
val previousItem = getItem(bindingAdapterPosition - 1)
|
||||||
if (previousItem.eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
if (previousItem.eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||||
val previousMessage = previousItem.eventLog.chatMessage
|
val previousMessage = previousItem.eventLog.chatMessage
|
||||||
if (previousMessage != null && previousMessage.fromAddress.weakEqual(chatMessage.fromAddress)) {
|
if (previousMessage != null && previousMessage.fromAddress.weakEqual(chatMessage.fromAddress)) {
|
||||||
|
@ -246,8 +253,8 @@ class ChatMessagesListAdapter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (adapterPosition >= 0 && adapterPosition < itemCount - 1) {
|
if (bindingAdapterPosition >= 0 && bindingAdapterPosition < itemCount - 1) {
|
||||||
val nextItem = getItem(adapterPosition + 1)
|
val nextItem = getItem(bindingAdapterPosition + 1)
|
||||||
if (nextItem.eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
if (nextItem.eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||||
val nextMessage = nextItem.eventLog.chatMessage
|
val nextMessage = nextItem.eventLog.chatMessage
|
||||||
if (nextMessage != null && nextMessage.fromAddress.weakEqual(chatMessage.fromAddress)) {
|
if (nextMessage != null && nextMessage.fromAddress.weakEqual(chatMessage.fromAddress)) {
|
||||||
|
@ -272,9 +279,8 @@ class ChatMessagesListAdapter(
|
||||||
|
|
||||||
val itemSize = AppUtils.getDimension(R.dimen.chat_message_popup_item_height).toInt()
|
val itemSize = AppUtils.getDimension(R.dimen.chat_message_popup_item_height).toInt()
|
||||||
var totalSize = itemSize * 7
|
var totalSize = itemSize * 7
|
||||||
if (chatMessage.chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) ||
|
if (chatMessage.chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) {
|
||||||
chatMessage.state == ChatMessage.State.NotDelivered
|
// No message id
|
||||||
) { // No message id
|
|
||||||
popupView.imdnHidden = true
|
popupView.imdnHidden = true
|
||||||
totalSize -= itemSize
|
totalSize -= itemSize
|
||||||
}
|
}
|
||||||
|
@ -347,7 +353,7 @@ class ChatMessagesListAdapter(
|
||||||
private fun resendMessage() {
|
private fun resendMessage() {
|
||||||
val chatMessage = binding.data?.chatMessage
|
val chatMessage = binding.data?.chatMessage
|
||||||
if (chatMessage != null) {
|
if (chatMessage != null) {
|
||||||
chatMessage.userData = adapterPosition
|
chatMessage.userData = bindingAdapterPosition
|
||||||
resendMessageEvent.value = Event(chatMessage)
|
resendMessageEvent.value = Event(chatMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -389,7 +395,7 @@ class ChatMessagesListAdapter(
|
||||||
private fun deleteMessage() {
|
private fun deleteMessage() {
|
||||||
val chatMessage = binding.data?.chatMessage
|
val chatMessage = binding.data?.chatMessage
|
||||||
if (chatMessage != null) {
|
if (chatMessage != null) {
|
||||||
chatMessage.userData = adapterPosition
|
chatMessage.userData = bindingAdapterPosition
|
||||||
deleteMessageEvent.value = Event(chatMessage)
|
deleteMessageEvent.value = Event(chatMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -417,15 +423,14 @@ class ChatMessagesListAdapter(
|
||||||
// This is for item selection through ListTopBarFragment
|
// This is for item selection through ListTopBarFragment
|
||||||
selectionListViewModel = selectionViewModel
|
selectionListViewModel = selectionViewModel
|
||||||
selectionViewModel.isEditionEnabled.observe(
|
selectionViewModel.isEditionEnabled.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
position = adapterPosition
|
position = bindingAdapterPosition
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
binding.setClickListener {
|
binding.setClickListener {
|
||||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||||
selectionViewModel.onToggleSelect(adapterPosition)
|
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -452,8 +457,18 @@ private class ChatMessageDiffCallback : DiffUtil.ItemCallback<EventLogData>() {
|
||||||
oldItem: EventLogData,
|
oldItem: EventLogData,
|
||||||
newItem: EventLogData
|
newItem: EventLogData
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return if (newItem.eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
return if (oldItem.eventLog.type == EventLog.Type.ConferenceChatMessage &&
|
||||||
newItem.eventLog.chatMessage?.state == ChatMessage.State.Displayed
|
newItem.eventLog.type == EventLog.Type.ConferenceChatMessage
|
||||||
} else true
|
) {
|
||||||
|
val oldData = (oldItem.data as ChatMessageData)
|
||||||
|
val newData = (newItem.data as ChatMessageData)
|
||||||
|
|
||||||
|
val previous = oldData.hasPreviousMessage == newData.hasPreviousMessage
|
||||||
|
val next = oldData.hasNextMessage == newData.hasNextMessage
|
||||||
|
newItem.eventLog.chatMessage?.state == ChatMessage.State.Displayed && previous && next
|
||||||
|
} else {
|
||||||
|
oldItem.eventLog.type != EventLog.Type.ConferenceChatMessage &&
|
||||||
|
newItem.eventLog.type != EventLog.Type.ConferenceChatMessage
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,17 +73,16 @@ class ChatRoomsListAdapter(
|
||||||
// This is for item selection through ListTopBarFragment
|
// This is for item selection through ListTopBarFragment
|
||||||
selectionListViewModel = selectionViewModel
|
selectionListViewModel = selectionViewModel
|
||||||
selectionViewModel.isEditionEnabled.observe(
|
selectionViewModel.isEditionEnabled.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
position = adapterPosition
|
position = bindingAdapterPosition
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
forwardPending = isForwardPending
|
forwardPending = isForwardPending
|
||||||
|
|
||||||
setClickListener {
|
setClickListener {
|
||||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||||
selectionViewModel.onToggleSelect(adapterPosition)
|
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||||
} else {
|
} else {
|
||||||
selectedChatRoomEvent.value = Event(chatRoomViewModel.chatRoom)
|
selectedChatRoomEvent.value = Event(chatRoomViewModel.chatRoom)
|
||||||
}
|
}
|
||||||
|
@ -116,6 +115,6 @@ private class ChatRoomDiffCallback : DiffUtil.ItemCallback<ChatRoomViewModel>()
|
||||||
oldItem: ChatRoomViewModel,
|
oldItem: ChatRoomViewModel,
|
||||||
newItem: ChatRoomViewModel
|
newItem: ChatRoomViewModel
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return newItem.unreadMessagesCount.value == 0
|
return false // To force redraw when contacts are updated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,6 @@ import org.linphone.utils.ImageUtils
|
||||||
class ChatMessageContentData(
|
class ChatMessageContentData(
|
||||||
private val chatMessage: ChatMessage,
|
private val chatMessage: ChatMessage,
|
||||||
private val contentIndex: Int,
|
private val contentIndex: Int,
|
||||||
|
|
||||||
) {
|
) {
|
||||||
var listener: OnContentClickedListener? = null
|
var listener: OnContentClickedListener? = null
|
||||||
|
|
||||||
|
@ -60,7 +59,6 @@ class ChatMessageContentData(
|
||||||
|
|
||||||
val fileName = MutableLiveData<String>()
|
val fileName = MutableLiveData<String>()
|
||||||
val filePath = MutableLiveData<String>()
|
val filePath = MutableLiveData<String>()
|
||||||
val fileSize = MutableLiveData<String>()
|
|
||||||
|
|
||||||
val downloadable = MutableLiveData<Boolean>()
|
val downloadable = MutableLiveData<Boolean>()
|
||||||
val downloadEnabled = MutableLiveData<Boolean>()
|
val downloadEnabled = MutableLiveData<Boolean>()
|
||||||
|
@ -72,13 +70,11 @@ class ChatMessageContentData(
|
||||||
val formattedDuration = MutableLiveData<String>()
|
val formattedDuration = MutableLiveData<String>()
|
||||||
val voiceRecordPlayingPosition = MutableLiveData<Int>()
|
val voiceRecordPlayingPosition = MutableLiveData<Int>()
|
||||||
val isVoiceRecordPlaying = MutableLiveData<Boolean>()
|
val isVoiceRecordPlaying = MutableLiveData<Boolean>()
|
||||||
var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
|
|
||||||
|
|
||||||
val isAlone: Boolean
|
val isAlone: Boolean
|
||||||
get() {
|
get() {
|
||||||
var count = 0
|
var count = 0
|
||||||
for (content in chatMessage.contents) {
|
for (content in chatMessage.contents) {
|
||||||
val content = getContent()
|
|
||||||
if (content.isFileTransfer || content.isFile) {
|
if (content.isFileTransfer || content.isFile) {
|
||||||
count += 1
|
count += 1
|
||||||
}
|
}
|
||||||
|
@ -86,7 +82,9 @@ class ChatMessageContentData(
|
||||||
return count == 1
|
return count == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
var isFileEncrypted: Boolean = false
|
private var isFileEncrypted: Boolean = false
|
||||||
|
|
||||||
|
private var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
|
||||||
|
|
||||||
private lateinit var voiceRecordingPlayer: Player
|
private lateinit var voiceRecordingPlayer: Player
|
||||||
private val playerListener = PlayerListener {
|
private val playerListener = PlayerListener {
|
||||||
|
@ -145,13 +143,7 @@ class ChatMessageContentData(
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
|
|
||||||
val path = filePath.value.orEmpty()
|
deletePlainFilePath()
|
||||||
if (path.isNotEmpty() && isFileEncrypted) {
|
|
||||||
Log.i("[Content] Deleting file used for preview: $path")
|
|
||||||
FileUtils.deleteFile(path)
|
|
||||||
filePath.value = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
chatMessage.removeListener(chatMessageListener)
|
chatMessage.removeListener(chatMessageListener)
|
||||||
|
|
||||||
if (this::voiceRecordingPlayer.isInitialized) {
|
if (this::voiceRecordingPlayer.isInitialized) {
|
||||||
|
@ -181,9 +173,22 @@ class ChatMessageContentData(
|
||||||
listener?.onContentClicked(getContent())
|
listener?.onContentClicked(getContent())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun deletePlainFilePath() {
|
||||||
|
val path = filePath.value.orEmpty()
|
||||||
|
if (path.isNotEmpty() && isFileEncrypted) {
|
||||||
|
Log.i("[Content] Deleting file used for preview: $path")
|
||||||
|
FileUtils.deleteFile(path)
|
||||||
|
filePath.value = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateContent() {
|
private fun updateContent() {
|
||||||
|
Log.i("[Content] Updating content")
|
||||||
|
deletePlainFilePath()
|
||||||
|
|
||||||
val content = getContent()
|
val content = getContent()
|
||||||
isFileEncrypted = content.isFileEncrypted
|
isFileEncrypted = content.isFileEncrypted
|
||||||
|
Log.i("[Content] Is ${if (content.isFile) "file" else "file transfer"} content encrypted ? $isFileEncrypted")
|
||||||
|
|
||||||
filePath.value = ""
|
filePath.value = ""
|
||||||
fileName.value = if (content.name.isNullOrEmpty() && !content.filePath.isNullOrEmpty()) {
|
fileName.value = if (content.name.isNullOrEmpty() && !content.filePath.isNullOrEmpty()) {
|
||||||
|
@ -193,14 +198,18 @@ class ChatMessageContentData(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display download size and underline text
|
// Display download size and underline text
|
||||||
fileSize.value = AppUtils.bytesToDisplayableSize(content.fileSize.toLong())
|
val fileSize = AppUtils.bytesToDisplayableSize(content.fileSize.toLong())
|
||||||
val spannable = SpannableString("${AppUtils.getString(R.string.chat_message_download_file)} (${fileSize.value})")
|
val spannable = SpannableString("${AppUtils.getString(R.string.chat_message_download_file)} ($fileSize)")
|
||||||
spannable.setSpan(UnderlineSpan(), 0, spannable.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
spannable.setSpan(UnderlineSpan(), 0, spannable.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
downloadLabel.value = spannable
|
downloadLabel.value = spannable
|
||||||
|
|
||||||
if (content.isFile || (content.isFileTransfer && chatMessage.isOutgoing)) {
|
if (content.isFile || (content.isFileTransfer && chatMessage.isOutgoing)) {
|
||||||
Log.i("[Content] Is content encrypted ? $isFileEncrypted")
|
val path = if (isFileEncrypted) {
|
||||||
val path = if (isFileEncrypted) content.plainFilePath else content.filePath ?: ""
|
Log.i("[Content] Content is encrypted, requesting plain file path")
|
||||||
|
content.plainFilePath
|
||||||
|
} else {
|
||||||
|
content.filePath ?: ""
|
||||||
|
}
|
||||||
downloadable.value = content.filePath.orEmpty().isEmpty()
|
downloadable.value = content.filePath.orEmpty().isEmpty()
|
||||||
|
|
||||||
if (path.isNotEmpty()) {
|
if (path.isNotEmpty()) {
|
||||||
|
@ -226,7 +235,7 @@ class ChatMessageContentData(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.w("[Content] Found content with empty path...")
|
Log.w("[Content] Found ${if (content.isFile) "file" else "file transfer"} content with empty path...")
|
||||||
isImage.value = false
|
isImage.value = false
|
||||||
isVideo.value = false
|
isVideo.value = false
|
||||||
isAudio.value = false
|
isAudio.value = false
|
||||||
|
@ -297,8 +306,9 @@ class ChatMessageContentData(
|
||||||
|
|
||||||
private fun initVoiceRecordPlayer() {
|
private fun initVoiceRecordPlayer() {
|
||||||
Log.i("[Voice Recording] Creating player for voice record")
|
Log.i("[Voice Recording] Creating player for voice record")
|
||||||
// Use speaker sound card to play recordings, otherwise use earpiece
|
// In case no headphones/headset is connected, use speaker sound card to play recordings, otherwise use earpiece
|
||||||
// If none are available, default one will be used
|
// If none are available, default one will be used
|
||||||
|
var headphonesCard: String? = null
|
||||||
var speakerCard: String? = null
|
var speakerCard: String? = null
|
||||||
var earpieceCard: String? = null
|
var earpieceCard: String? = null
|
||||||
for (device in coreContext.core.audioDevices) {
|
for (device in coreContext.core.audioDevices) {
|
||||||
|
@ -307,12 +317,14 @@ class ChatMessageContentData(
|
||||||
speakerCard = device.id
|
speakerCard = device.id
|
||||||
} else if (device.type == AudioDevice.Type.Earpiece) {
|
} else if (device.type == AudioDevice.Type.Earpiece) {
|
||||||
earpieceCard = device.id
|
earpieceCard = device.id
|
||||||
|
} else if (device.type == AudioDevice.Type.Headphones || device.type == AudioDevice.Type.Headset) {
|
||||||
|
headphonesCard = device.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.i("[Voice Recording] Found speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]")
|
Log.i("[Voice Recording] Found headset/headphones sound card [$headphonesCard], speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]")
|
||||||
|
|
||||||
val localPlayer = coreContext.core.createLocalPlayer(speakerCard ?: earpieceCard, null, null)
|
val localPlayer = coreContext.core.createLocalPlayer(headphonesCard ?: speakerCard ?: earpieceCard, null, null)
|
||||||
if (localPlayer != null) {
|
if (localPlayer != null) {
|
||||||
voiceRecordingPlayer = localPlayer
|
voiceRecordingPlayer = localPlayer
|
||||||
} else {
|
} else {
|
||||||
|
@ -321,8 +333,7 @@ class ChatMessageContentData(
|
||||||
}
|
}
|
||||||
voiceRecordingPlayer.addListener(playerListener)
|
voiceRecordingPlayer.addListener(playerListener)
|
||||||
|
|
||||||
val content = getContent()
|
val path = filePath.value
|
||||||
val path = if (content.isFileEncrypted) content.plainFilePath else content.filePath ?: ""
|
|
||||||
voiceRecordingPlayer.open(path.orEmpty())
|
voiceRecordingPlayer.open(path.orEmpty())
|
||||||
voiceRecordDuration.value = voiceRecordingPlayer.duration
|
voiceRecordDuration.value = voiceRecordingPlayer.duration
|
||||||
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(voiceRecordingPlayer.duration) // is already in milliseconds
|
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(voiceRecordingPlayer.duration) // is already in milliseconds
|
||||||
|
@ -346,4 +357,6 @@ class ChatMessageContentData(
|
||||||
|
|
||||||
interface OnContentClickedListener {
|
interface OnContentClickedListener {
|
||||||
fun onContentClicked(content: Content)
|
fun onContentClicked(content: Content)
|
||||||
|
|
||||||
|
fun onSipAddressClicked(sipUri: String)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,12 +24,14 @@ import android.text.Spannable
|
||||||
import android.text.util.Linkify
|
import android.text.util.Linkify
|
||||||
import androidx.core.text.util.LinkifyCompat
|
import androidx.core.text.util.LinkifyCompat
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import java.util.regex.Pattern
|
||||||
import org.linphone.R
|
import org.linphone.R
|
||||||
import org.linphone.contact.GenericContactData
|
import org.linphone.contact.GenericContactData
|
||||||
import org.linphone.core.ChatMessage
|
import org.linphone.core.ChatMessage
|
||||||
import org.linphone.core.ChatMessageListenerStub
|
import org.linphone.core.ChatMessageListenerStub
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.utils.AppUtils
|
import org.linphone.utils.AppUtils
|
||||||
|
import org.linphone.utils.PatternClickableSpan
|
||||||
import org.linphone.utils.TimestampUtils
|
import org.linphone.utils.TimestampUtils
|
||||||
|
|
||||||
class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMessage.fromAddress) {
|
class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMessage.fromAddress) {
|
||||||
|
@ -59,6 +61,9 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
|
||||||
|
|
||||||
val replyData = MutableLiveData<ChatMessageData>()
|
val replyData = MutableLiveData<ChatMessageData>()
|
||||||
|
|
||||||
|
var hasPreviousMessage = false
|
||||||
|
var hasNextMessage = false
|
||||||
|
|
||||||
private var countDownTimer: CountDownTimer? = null
|
private var countDownTimer: CountDownTimer? = null
|
||||||
|
|
||||||
private val listener = object : ChatMessageListenerStub() {
|
private val listener = object : ChatMessageListenerStub() {
|
||||||
|
@ -106,6 +111,11 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateBubbleBackground(hasPrevious: Boolean, hasNext: Boolean) {
|
fun updateBubbleBackground(hasPrevious: Boolean, hasNext: Boolean) {
|
||||||
|
hasPreviousMessage = hasPrevious
|
||||||
|
hasNextMessage = hasNext
|
||||||
|
hideTime.value = false
|
||||||
|
hideAvatar.value = false
|
||||||
|
|
||||||
if (hasPrevious) {
|
if (hasPrevious) {
|
||||||
hideTime.value = true
|
hideTime.value = true
|
||||||
}
|
}
|
||||||
|
@ -165,16 +175,25 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
|
||||||
val list = arrayListOf<ChatMessageContentData>()
|
val list = arrayListOf<ChatMessageContentData>()
|
||||||
|
|
||||||
val contentsList = chatMessage.contents
|
val contentsList = chatMessage.contents
|
||||||
for (index in 0 until contentsList.size) {
|
for (index in contentsList.indices) {
|
||||||
val content = contentsList[index]
|
val content = contentsList[index]
|
||||||
if (content.isFileTransfer || content.isFile) {
|
if (content.isFileTransfer || content.isFile) {
|
||||||
val data = ChatMessageContentData(chatMessage, index)
|
val data = ChatMessageContentData(chatMessage, index)
|
||||||
data.listener = contentListener
|
data.listener = contentListener
|
||||||
list.add(data)
|
list.add(data)
|
||||||
} else if (content.isText) {
|
} else if (content.isText) {
|
||||||
val spannable = Spannable.Factory.getInstance().newSpannable(content.utf8Text)
|
val spannable = Spannable.Factory.getInstance().newSpannable(content.utf8Text?.trim())
|
||||||
LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS)
|
LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS)
|
||||||
text.value = spannable
|
text.value = PatternClickableSpan()
|
||||||
|
.add(
|
||||||
|
Pattern.compile("(sips?):([^@]+)(?:@([^ ]+))?"),
|
||||||
|
object : PatternClickableSpan.SpannableClickedListener {
|
||||||
|
override fun onSpanClicked(text: String) {
|
||||||
|
Log.i("[Chat Message Data] Clicked on SIP URI: $text")
|
||||||
|
contentListener?.onSipAddressClicked(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).build(spannable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -93,53 +93,47 @@ class ChatRoomCreationFragment : SecureFragment<ChatRoomCreationFragmentBinding>
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.contactsList.observe(
|
viewModel.contactsList.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
adapter.submitList(it)
|
adapter.submitList(it)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.isEncrypted.observe(
|
viewModel.isEncrypted.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
adapter.updateSecurity(it)
|
adapter.updateSecurity(it)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.sipContactsSelected.observe(
|
viewModel.sipContactsSelected.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
viewModel.updateContactsList()
|
viewModel.updateContactsList()
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.selectedAddresses.observe(
|
viewModel.selectedAddresses.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
adapter.updateSelectedAddresses(it)
|
adapter.updateSelectedAddresses(it)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.chatRoomCreatedEvent.observe(
|
viewModel.chatRoomCreatedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { chatRoom ->
|
it.consume { chatRoom ->
|
||||||
sharedViewModel.selectedChatRoom.value = chatRoom
|
sharedViewModel.selectedChatRoom.value = chatRoom
|
||||||
navigateToChatRoom(AppUtils.createBundleWithSharedTextAndFiles(sharedViewModel))
|
navigateToChatRoom(AppUtils.createBundleWithSharedTextAndFiles(sharedViewModel))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.filter.observe(
|
viewModel.filter.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
viewModel.applyFilter()
|
viewModel.applyFilter()
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
adapter.selectedContact.observe(
|
adapter.selectedContact.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { searchResult ->
|
it.consume { searchResult ->
|
||||||
if (createGroup) {
|
if (createGroup) {
|
||||||
viewModel.toggleSelectionForSearchResult(searchResult)
|
viewModel.toggleSelectionForSearchResult(searchResult)
|
||||||
|
@ -148,7 +142,6 @@ class ChatRoomCreationFragment : SecureFragment<ChatRoomCreationFragmentBinding>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
addParticipantsFromSharedViewModel()
|
addParticipantsFromSharedViewModel()
|
||||||
|
|
||||||
|
@ -160,13 +153,12 @@ class ChatRoomCreationFragment : SecureFragment<ChatRoomCreationFragmentBinding>
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.onErrorEvent.observe(
|
viewModel.onErrorEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { messageResourceId ->
|
it.consume { messageResourceId ->
|
||||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
if (!PermissionHelper.get().hasReadContactsPermission()) {
|
if (!PermissionHelper.get().hasReadContactsPermission()) {
|
||||||
Log.i("[Chat Room Creation] Asking for READ_CONTACTS permission")
|
Log.i("[Chat Room Creation] Asking for READ_CONTACTS permission")
|
||||||
|
|
|
@ -29,19 +29,20 @@ import android.provider.MediaStore
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.ViewTreeObserver.OnGlobalLayoutListener
|
import android.view.ViewTreeObserver.OnGlobalLayoutListener
|
||||||
import android.widget.PopupWindow
|
import android.widget.PopupWindow
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.view.doOnPreDraw
|
import androidx.core.view.doOnPreDraw
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.databinding.ViewDataBinding
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.IllegalArgumentException
|
import java.lang.IllegalArgumentException
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||||
import org.linphone.R
|
import org.linphone.R
|
||||||
|
@ -52,6 +53,7 @@ import org.linphone.activities.main.chat.adapters.ChatMessagesListAdapter
|
||||||
import org.linphone.activities.main.chat.data.ChatMessageData
|
import org.linphone.activities.main.chat.data.ChatMessageData
|
||||||
import org.linphone.activities.main.chat.data.EventLogData
|
import org.linphone.activities.main.chat.data.EventLogData
|
||||||
import org.linphone.activities.main.chat.viewmodels.*
|
import org.linphone.activities.main.chat.viewmodels.*
|
||||||
|
import org.linphone.activities.main.chat.views.RichEditTextSendListener
|
||||||
import org.linphone.activities.main.fragments.MasterFragment
|
import org.linphone.activities.main.fragments.MasterFragment
|
||||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||||
|
@ -107,6 +109,20 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
if (::sharedViewModel.isInitialized) {
|
||||||
|
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||||
|
if (chatRoom != null) {
|
||||||
|
outState.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly())
|
||||||
|
outState.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly())
|
||||||
|
Log.i("[Chat Room] Saving current chat room local & remote addresses in save instance state")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w("[Chat Room] Can't save instance state, sharedViewModel hasn't been initialized yet")
|
||||||
|
}
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
|
@ -119,20 +135,18 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
|
|
||||||
useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false
|
useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false
|
||||||
|
|
||||||
view.doOnPreDraw {
|
val localSipUri = arguments?.getString("LocalSipUri") ?: savedInstanceState?.getString("LocalSipUri")
|
||||||
// Notifies fragment is ready to be drawn
|
val remoteSipUri = arguments?.getString("RemoteSipUri") ?: savedInstanceState?.getString("RemoteSipUri")
|
||||||
sharedViewModel.chatRoomFragmentOpenedEvent.value = Event(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
val localSipUri = arguments?.getString("LocalSipUri")
|
|
||||||
val remoteSipUri = arguments?.getString("RemoteSipUri")
|
|
||||||
|
|
||||||
val textToShare = arguments?.getString("TextToShare")
|
val textToShare = arguments?.getString("TextToShare")
|
||||||
val filesToShare = arguments?.getStringArrayList("FilesToShare")
|
val filesToShare = arguments?.getStringArrayList("FilesToShare")
|
||||||
|
|
||||||
|
if (remoteSipUri != null && arguments?.getString("RemoteSipUri") == null) {
|
||||||
|
Log.w("[Chat Room] Chat room will be restored from saved instance state")
|
||||||
|
}
|
||||||
arguments?.clear()
|
arguments?.clear()
|
||||||
if (localSipUri != null && remoteSipUri != null) {
|
if (localSipUri != null && remoteSipUri != null) {
|
||||||
Log.i("[Chat Room] Found local [$localSipUri] & remote [$remoteSipUri] addresses in arguments")
|
Log.i("[Chat Room] Found local [$localSipUri] & remote [$remoteSipUri] addresses in arguments or saved instance state")
|
||||||
|
|
||||||
val localAddress = Factory.instance().createAddress(localSipUri)
|
val localAddress = Factory.instance().createAddress(localSipUri)
|
||||||
val remoteSipAddress = Factory.instance().createAddress(remoteSipUri)
|
val remoteSipAddress = Factory.instance().createAddress(remoteSipUri)
|
||||||
|
@ -152,14 +166,29 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
view.doOnPreDraw {
|
||||||
|
// Notifies fragment is ready to be drawn
|
||||||
|
sharedViewModel.chatRoomFragmentOpenedEvent.value = Event(true)
|
||||||
|
}
|
||||||
|
|
||||||
Compatibility.setLocusIdInContentCaptureSession(binding.root, chatRoom)
|
Compatibility.setLocusIdInContentCaptureSession(binding.root, chatRoom)
|
||||||
|
|
||||||
isSecure = chatRoom.currentParams.encryptionEnabled()
|
isSecure = chatRoom.currentParams.isEncryptionEnabled
|
||||||
|
|
||||||
viewModel = ViewModelProvider(
|
val chatRoomsListViewModel: ChatRoomsListViewModel = requireActivity().run {
|
||||||
|
ViewModelProvider(this)[ChatRoomsListViewModel::class.java]
|
||||||
|
}
|
||||||
|
val chatRoomViewModel = chatRoomsListViewModel.chatRooms.value.orEmpty().find {
|
||||||
|
it.chatRoom == chatRoom
|
||||||
|
}
|
||||||
|
if (chatRoomViewModel == null) {
|
||||||
|
Log.w("[Chat Room] Couldn't find existing view model, will create a new one!")
|
||||||
|
}
|
||||||
|
viewModel = chatRoomViewModel ?: ViewModelProvider(
|
||||||
this,
|
this,
|
||||||
ChatRoomViewModelFactory(chatRoom)
|
ChatRoomViewModelFactory(chatRoom)
|
||||||
)[ChatRoomViewModel::class.java]
|
)[ChatRoomViewModel::class.java]
|
||||||
|
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
chatSendingViewModel = ViewModelProvider(
|
chatSendingViewModel = ViewModelProvider(
|
||||||
|
@ -193,24 +222,38 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
.addOnGlobalLayoutListener(
|
.addOnGlobalLayoutListener(
|
||||||
object : OnGlobalLayoutListener {
|
object : OnGlobalLayoutListener {
|
||||||
override fun onGlobalLayout() {
|
override fun onGlobalLayout() {
|
||||||
|
if (isBindingAvailable()) {
|
||||||
binding.chatMessagesList
|
binding.chatMessagesList
|
||||||
.viewTreeObserver
|
.viewTreeObserver
|
||||||
.removeOnGlobalLayoutListener(this)
|
.removeOnGlobalLayoutListener(this)
|
||||||
Log.i("[Chat Room] Messages have been displayed, scrolling to first unread message if any")
|
Log.i("[Chat Room] Messages have been displayed, scrolling to first unread message if any")
|
||||||
scrollToFirstUnreadMessageOrBottom(false)
|
scrollToFirstUnreadMessageOrBottom(false)
|
||||||
|
} else {
|
||||||
|
Log.e("[Chat Room] Binding not available in onGlobalLayout callback!")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Swipe action
|
// Swipe action
|
||||||
/*val swipeConfiguration = RecyclerViewSwipeConfiguration()
|
val swipeConfiguration = RecyclerViewSwipeConfiguration()
|
||||||
swipeConfiguration.leftToRightAction = RecyclerViewSwipeConfiguration.Action(icon = R.drawable.menu_reply_default)
|
// Reply action can only be done on a ChatMessageEventLog
|
||||||
|
swipeConfiguration.leftToRightAction = RecyclerViewSwipeConfiguration.Action(
|
||||||
|
text = requireContext().getString(R.string.chat_message_context_menu_reply),
|
||||||
|
backgroundColor = ContextCompat.getColor(requireContext(), R.color.light_grey_color),
|
||||||
|
preventFor = ChatMessagesListAdapter.EventViewHolder::class.java
|
||||||
|
)
|
||||||
|
// Delete action can be done on any EventLog
|
||||||
|
swipeConfiguration.rightToLeftAction = RecyclerViewSwipeConfiguration.Action(
|
||||||
|
text = requireContext().getString(R.string.chat_message_context_menu_delete),
|
||||||
|
backgroundColor = ContextCompat.getColor(requireContext(), R.color.red_color)
|
||||||
|
)
|
||||||
val swipeListener = object : RecyclerViewSwipeListener {
|
val swipeListener = object : RecyclerViewSwipeListener {
|
||||||
override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) {
|
override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) {
|
||||||
adapter.notifyItemChanged(viewHolder.adapterPosition)
|
adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
|
||||||
|
|
||||||
val chatMessageEventLog = adapter.currentList[viewHolder.adapterPosition]
|
val chatMessageEventLog = adapter.currentList[viewHolder.bindingAdapterPosition]
|
||||||
val chatMessage = chatMessageEventLog.chatMessage
|
val chatMessage = chatMessageEventLog.eventLog.chatMessage
|
||||||
if (chatMessage != null) {
|
if (chatMessage != null) {
|
||||||
chatSendingViewModel.pendingChatMessageToReplyTo.value?.destroy()
|
chatSendingViewModel.pendingChatMessageToReplyTo.value?.destroy()
|
||||||
chatSendingViewModel.pendingChatMessageToReplyTo.value =
|
chatSendingViewModel.pendingChatMessageToReplyTo.value =
|
||||||
|
@ -219,10 +262,16 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) {}
|
override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) {
|
||||||
|
val position = viewHolder.bindingAdapterPosition
|
||||||
|
// adapter.notifyItemRemoved(viewHolder.bindingAdapterPosition)
|
||||||
|
|
||||||
|
val eventLog = adapter.currentList[position]
|
||||||
|
addDeleteMessageTaskToQueue(eventLog, position)
|
||||||
}
|
}
|
||||||
RecyclerViewSwipeUtils(ItemTouchHelper.RIGHT, swipeConfiguration, swipeListener)
|
}
|
||||||
.attachToRecyclerView(binding.chatMessagesList)*/
|
RecyclerViewSwipeUtils(ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT, swipeConfiguration, swipeListener)
|
||||||
|
.attachToRecyclerView(binding.chatMessagesList)
|
||||||
|
|
||||||
val chatScrollListener = object : ChatScrollListener(layoutManager) {
|
val chatScrollListener = object : ChatScrollListener(layoutManager) {
|
||||||
override fun onLoadMore(totalItemsCount: Int) {
|
override fun onLoadMore(totalItemsCount: Int) {
|
||||||
|
@ -236,7 +285,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
|
|
||||||
override fun onScrolledToEnd() {
|
override fun onScrolledToEnd() {
|
||||||
viewModel.isUserScrollingUp.value = false
|
viewModel.isUserScrollingUp.value = false
|
||||||
if (viewModel.unreadMessagesCount.value != 0) {
|
if (viewModel.unreadMessagesCount.value != 0 && coreContext.notificationsManager.currentlyDisplayedChatRoomAddress != null) {
|
||||||
Log.i("[Chat Room] User has scrolled to the latest message, mark chat room as read")
|
Log.i("[Chat Room] User has scrolled to the latest message, mark chat room as read")
|
||||||
viewModel.chatRoom.markAsRead()
|
viewModel.chatRoom.markAsRead()
|
||||||
}
|
}
|
||||||
|
@ -245,69 +294,66 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
binding.chatMessagesList.addOnScrollListener(chatScrollListener)
|
binding.chatMessagesList.addOnScrollListener(chatScrollListener)
|
||||||
|
|
||||||
chatSendingViewModel.textToSend.observe(
|
chatSendingViewModel.textToSend.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
chatSendingViewModel.onTextToSendChanged(it)
|
chatSendingViewModel.onTextToSendChanged(it)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
chatSendingViewModel.requestRecordAudioPermissionEvent.observe(
|
chatSendingViewModel.requestRecordAudioPermissionEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
Log.i("[Chat Room] Asking for RECORD_AUDIO permission")
|
Log.i("[Chat Room] Asking for RECORD_AUDIO permission")
|
||||||
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 2)
|
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
listViewModel.events.observe(
|
listViewModel.events.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{ events ->
|
) { events ->
|
||||||
adapter.setUnreadMessageCount(viewModel.chatRoom.unreadMessagesCount, viewModel.isUserScrollingUp.value == true)
|
adapter.setUnreadMessageCount(
|
||||||
|
viewModel.chatRoom.unreadMessagesCount,
|
||||||
|
viewModel.isUserScrollingUp.value == true
|
||||||
|
)
|
||||||
adapter.submitList(events)
|
adapter.submitList(events)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
listViewModel.messageUpdatedEvent.observe(
|
listViewModel.messageUpdatedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { position ->
|
it.consume { position ->
|
||||||
adapter.notifyItemChanged(position)
|
adapter.notifyItemChanged(position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
listViewModel.requestWriteExternalStoragePermissionEvent.observe(
|
listViewModel.requestWriteExternalStoragePermissionEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
|
requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
adapter.deleteMessageEvent.observe(
|
adapter.deleteMessageEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { chatMessage ->
|
it.consume { chatMessage ->
|
||||||
listViewModel.deleteMessage(chatMessage)
|
listViewModel.deleteMessage(chatMessage)
|
||||||
|
viewModel.updateLastMessageToDisplay()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
adapter.resendMessageEvent.observe(
|
adapter.resendMessageEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { chatMessage ->
|
it.consume { chatMessage ->
|
||||||
listViewModel.resendMessage(chatMessage)
|
listViewModel.resendMessage(chatMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
adapter.forwardMessageEvent.observe(
|
adapter.forwardMessageEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { chatMessage ->
|
it.consume { chatMessage ->
|
||||||
// Remove observer before setting the message to forward
|
// Remove observer before setting the message to forward
|
||||||
// as we don't want to forward it in this chat room
|
// as we don't want to forward it in this chat room
|
||||||
|
@ -323,44 +369,42 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
adapter.replyMessageEvent.observe(
|
adapter.replyMessageEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { chatMessage ->
|
it.consume { chatMessage ->
|
||||||
chatSendingViewModel.pendingChatMessageToReplyTo.value?.destroy()
|
chatSendingViewModel.pendingChatMessageToReplyTo.value?.destroy()
|
||||||
chatSendingViewModel.pendingChatMessageToReplyTo.value = ChatMessageData(chatMessage)
|
chatSendingViewModel.pendingChatMessageToReplyTo.value =
|
||||||
|
ChatMessageData(chatMessage)
|
||||||
chatSendingViewModel.isPendingAnswer.value = true
|
chatSendingViewModel.isPendingAnswer.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
adapter.showImdnForMessageEvent.observe(
|
adapter.showImdnForMessageEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { chatMessage ->
|
it.consume { chatMessage ->
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
args.putString("MessageId", chatMessage.messageId)
|
args.putString("MessageId", chatMessage.messageId)
|
||||||
navigateToImdn(args)
|
navigateToImdn(args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
adapter.addSipUriToContactEvent.observe(
|
adapter.addSipUriToContactEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { sipUri ->
|
it.consume { sipUri ->
|
||||||
Log.i("[Chat Room] Going to contacts list with SIP URI to add: $sipUri")
|
Log.i("[Chat Room] Going to contacts list with SIP URI to add: $sipUri")
|
||||||
sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event(R.id.masterChatRoomsFragment)
|
sharedViewModel.updateContactsAnimationsBasedOnDestination.value =
|
||||||
|
Event(R.id.masterChatRoomsFragment)
|
||||||
navigateToContacts(sipUri)
|
navigateToContacts(sipUri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
adapter.openContentEvent.observe(
|
adapter.openContentEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { content ->
|
it.consume { content ->
|
||||||
val path = content.filePath.orEmpty()
|
val path = content.filePath.orEmpty()
|
||||||
|
|
||||||
|
@ -372,7 +416,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
|
|
||||||
if (corePreferences.useInAppFileViewerForNonEncryptedFiles || content.isFileEncrypted) {
|
if (corePreferences.useInAppFileViewerForNonEncryptedFiles || content.isFileEncrypted) {
|
||||||
val preventScreenshots =
|
val preventScreenshots =
|
||||||
viewModel.chatRoom.currentParams.encryptionEnabled()
|
viewModel.chatRoom.currentParams.isEncryptionEnabled
|
||||||
when {
|
when {
|
||||||
FileUtils.isExtensionImage(path) -> navigateToImageFileViewer(
|
FileUtils.isExtensionImage(path) -> navigateToImageFileViewer(
|
||||||
preventScreenshots
|
preventScreenshots
|
||||||
|
@ -392,8 +436,14 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
else -> {
|
else -> {
|
||||||
if (content.isFileEncrypted) {
|
if (content.isFileEncrypted) {
|
||||||
Log.w("[Chat Message] File is encrypted and can't be opened in one of our viewers...")
|
Log.w("[Chat Message] File is encrypted and can't be opened in one of our viewers...")
|
||||||
showDialogForUserConsentBeforeExportingFileInThirdPartyApp(content)
|
showDialogForUserConsentBeforeExportingFileInThirdPartyApp(
|
||||||
} else if (!FileUtils.openFileInThirdPartyApp(requireActivity(), path)) {
|
content
|
||||||
|
)
|
||||||
|
} else if (!FileUtils.openFileInThirdPartyApp(
|
||||||
|
requireActivity(),
|
||||||
|
path
|
||||||
|
)
|
||||||
|
) {
|
||||||
showDialogToSuggestOpeningFileAsText()
|
showDialogToSuggestOpeningFileAsText()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -406,11 +456,23 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
adapter.sipUriClickedEvent.observe(
|
||||||
|
viewLifecycleOwner
|
||||||
|
) {
|
||||||
|
it.consume { sipUri ->
|
||||||
|
val args = Bundle()
|
||||||
|
args.putString("URI", sipUri)
|
||||||
|
args.putBoolean("Transfer", false)
|
||||||
|
// If auto start call setting is enabled, ignore it
|
||||||
|
args.putBoolean("SkipAutoCallStart", true)
|
||||||
|
navigateToDialer(args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
adapter.scrollToChatMessageEvent.observe(
|
adapter.scrollToChatMessageEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { chatMessage ->
|
it.consume { chatMessage ->
|
||||||
val events = listViewModel.events.value.orEmpty()
|
val events = listViewModel.events.value.orEmpty()
|
||||||
val eventLog = events.find { eventLog ->
|
val eventLog = events.find { eventLog ->
|
||||||
|
@ -430,7 +492,6 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
binding.setBackClickListener {
|
binding.setBackClickListener {
|
||||||
goBack()
|
goBack()
|
||||||
|
@ -493,8 +554,20 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.message.setControlEnterListener(object : RichEditTextSendListener {
|
||||||
|
override fun onControlEnterPressedAndReleased() {
|
||||||
|
Log.i("[Chat Room] Detected left control + enter key presses, sending message")
|
||||||
|
chatSendingViewModel.sendMessage()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
binding.setCancelReplyToClickListener {
|
||||||
|
chatSendingViewModel.cancelReply()
|
||||||
|
}
|
||||||
|
|
||||||
binding.setScrollToBottomClickListener {
|
binding.setScrollToBottomClickListener {
|
||||||
scrollToFirstUnreadMessageOrBottom(true)
|
scrollToFirstUnreadMessageOrBottom(true)
|
||||||
|
viewModel.isUserScrollingUp.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (textToShare?.isNotEmpty() == true) {
|
if (textToShare?.isNotEmpty() == true) {
|
||||||
|
@ -509,8 +582,8 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
}
|
}
|
||||||
|
|
||||||
sharedViewModel.richContentUri.observe(
|
sharedViewModel.richContentUri.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { uri ->
|
it.consume { uri ->
|
||||||
Log.i("[Chat] Found rich content URI: $uri")
|
Log.i("[Chat] Found rich content URI: $uri")
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
@ -524,18 +597,28 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
sharedViewModel.messageToForwardEvent.observe(
|
sharedViewModel.messageToForwardEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { chatMessage ->
|
it.consume { chatMessage ->
|
||||||
Log.i("[Chat Room] Found message to transfer")
|
Log.i("[Chat Room] Found message to transfer")
|
||||||
showForwardConfirmationDialog(chatMessage)
|
showForwardConfirmationDialog(chatMessage)
|
||||||
sharedViewModel.isPendingMessageForward.value = false
|
sharedViewModel.isPendingMessageForward.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
binding.stubbedMessageToReplyTo.setOnInflateListener { _, inflated ->
|
||||||
|
Log.i("[Chat Room] Replying to message layout inflated")
|
||||||
|
val binding = DataBindingUtil.bind<ViewDataBinding>(inflated)
|
||||||
|
binding?.lifecycleOwner = viewLifecycleOwner
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.stubbedVoiceRecording.setOnInflateListener { _, inflated ->
|
||||||
|
Log.i("[Chat Room] Voice recording layout inflated")
|
||||||
|
val binding = DataBindingUtil.bind<ViewDataBinding>(inflated)
|
||||||
|
binding?.lifecycleOwner = viewLifecycleOwner
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deleteItems(indexesOfItemToDelete: ArrayList<Int>) {
|
override fun deleteItems(indexesOfItemToDelete: ArrayList<Int>) {
|
||||||
|
@ -545,6 +628,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
list.add(eventLog)
|
list.add(eventLog)
|
||||||
}
|
}
|
||||||
listViewModel.deleteEventLogs(list)
|
listViewModel.deleteEventLogs(list)
|
||||||
|
viewModel.updateLastMessageToDisplay()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(
|
override fun onRequestPermissionsResult(
|
||||||
|
@ -607,6 +691,7 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun goBack() {
|
override fun goBack() {
|
||||||
|
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
||||||
if (!findNavController().popBackStack()) {
|
if (!findNavController().popBackStack()) {
|
||||||
if (sharedViewModel.isSlidingPaneSlideable.value == true) {
|
if (sharedViewModel.isSlidingPaneSlideable.value == true) {
|
||||||
if (_adapter != null) {
|
if (_adapter != null) {
|
||||||
|
@ -772,6 +857,35 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
popupWindow.showAsDropDown(binding.menu, 0, 0, Gravity.BOTTOM)
|
popupWindow.showAsDropDown(binding.menu, 0, 0, Gravity.BOTTOM)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addDeleteMessageTaskToQueue(eventLog: EventLogData, position: Int) {
|
||||||
|
val task = lifecycleScope.launch {
|
||||||
|
delay(2800) // Duration of Snackbar.LENGTH_LONG
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (isActive) {
|
||||||
|
Log.i("[Chat Room] Message/event deletion task is still active, proceed")
|
||||||
|
val chatMessage = eventLog.eventLog.chatMessage
|
||||||
|
if (chatMessage != null) {
|
||||||
|
Log.i("[Chat Room] Deleting message $chatMessage at position $position")
|
||||||
|
listViewModel.deleteMessage(chatMessage)
|
||||||
|
} else {
|
||||||
|
Log.i("[Chat Room] Deleting event $eventLog at position $position")
|
||||||
|
listViewModel.deleteEventLogs(arrayListOf(eventLog))
|
||||||
|
}
|
||||||
|
viewModel.updateLastMessageToDisplay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(requireActivity() as MainActivity).showSnackBar(
|
||||||
|
R.string.chat_message_removal_info,
|
||||||
|
R.string.chat_message_abort_removal
|
||||||
|
) {
|
||||||
|
Log.i("[Chat Room] Canceled message/event deletion task: $task for message/event at position $position")
|
||||||
|
adapter.notifyItemRangeChanged(position, adapter.itemCount - position)
|
||||||
|
task.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun scrollToFirstUnreadMessageOrBottom(smooth: Boolean) {
|
private fun scrollToFirstUnreadMessageOrBottom(smooth: Boolean) {
|
||||||
if (_adapter != null && adapter.itemCount > 0) {
|
if (_adapter != null && adapter.itemCount > 0) {
|
||||||
// Scroll to first unread message if any
|
// Scroll to first unread message if any
|
||||||
|
@ -858,11 +972,13 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
|
||||||
{
|
{
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
Log.w("[Chat Room] Content is encrypted, requesting plain file path")
|
||||||
val plainFilePath = content.plainFilePath
|
val plainFilePath = content.plainFilePath
|
||||||
Log.i("[Cht Room] Making a copy of [$plainFilePath] to the cache directory before exporting it")
|
Log.i("[Cht Room] Making a copy of [$plainFilePath] to the cache directory before exporting it")
|
||||||
val cacheCopyPath = FileUtils.copyFileToCache(plainFilePath)
|
val cacheCopyPath = FileUtils.copyFileToCache(plainFilePath)
|
||||||
if (cacheCopyPath != null) {
|
if (cacheCopyPath != null) {
|
||||||
Log.i("[Cht Room] Cache copy has been made: $cacheCopyPath")
|
Log.i("[Cht Room] Cache copy has been made: $cacheCopyPath")
|
||||||
|
FileUtils.deleteFile(plainFilePath)
|
||||||
if (!FileUtils.openFileInThirdPartyApp(requireActivity(), cacheCopyPath)) {
|
if (!FileUtils.openFileInThirdPartyApp(requireActivity(), cacheCopyPath)) {
|
||||||
showDialogToSuggestOpeningFileAsText()
|
showDialogToSuggestOpeningFileAsText()
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ class DevicesFragment : SecureFragment<ChatRoomDevicesFragmentBinding>() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isSecure = chatRoom.currentParams.encryptionEnabled()
|
isSecure = chatRoom.currentParams.isEncryptionEnabled
|
||||||
|
|
||||||
listViewModel = ViewModelProvider(
|
listViewModel = ViewModelProvider(
|
||||||
this,
|
this,
|
||||||
|
@ -66,4 +66,10 @@ class DevicesFragment : SecureFragment<ChatRoomDevicesFragmentBinding>() {
|
||||||
goBack()
|
goBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
|
||||||
|
listViewModel.updateParticipants()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ class GroupInfoFragment : SecureFragment<ChatRoomGroupInfoFragmentBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
val chatRoom: ChatRoom? = sharedViewModel.selectedGroupChatRoom.value
|
val chatRoom: ChatRoom? = sharedViewModel.selectedGroupChatRoom.value
|
||||||
isSecure = chatRoom?.currentParams?.encryptionEnabled() ?: false
|
isSecure = chatRoom?.currentParams?.isEncryptionEnabled ?: false
|
||||||
|
|
||||||
viewModel = ViewModelProvider(
|
viewModel = ViewModelProvider(
|
||||||
this,
|
this,
|
||||||
|
@ -84,36 +84,32 @@ class GroupInfoFragment : SecureFragment<ChatRoomGroupInfoFragmentBinding>() {
|
||||||
binding.participants.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
|
binding.participants.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
|
||||||
|
|
||||||
viewModel.participants.observe(
|
viewModel.participants.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
adapter.submitList(it)
|
adapter.submitList(it)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.isMeAdmin.observe(
|
viewModel.isMeAdmin.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{ isMeAdmin ->
|
) { isMeAdmin ->
|
||||||
adapter.showAdminControls(isMeAdmin && chatRoom != null)
|
adapter.showAdminControls(isMeAdmin && chatRoom != null)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.meAdminChangedEvent.observe(
|
viewModel.meAdminChangedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { isMeAdmin ->
|
it.consume { isMeAdmin ->
|
||||||
showMeAdminStateChanged(isMeAdmin)
|
showMeAdminStateChanged(isMeAdmin)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
adapter.participantRemovedEvent.observe(
|
adapter.participantRemovedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { participant ->
|
it.consume { participant ->
|
||||||
viewModel.removeParticipant(participant)
|
viewModel.removeParticipant(participant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
addParticipantsFromSharedViewModel()
|
addParticipantsFromSharedViewModel()
|
||||||
|
|
||||||
|
@ -122,22 +118,20 @@ class GroupInfoFragment : SecureFragment<ChatRoomGroupInfoFragmentBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.createdChatRoomEvent.observe(
|
viewModel.createdChatRoomEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { chatRoom ->
|
it.consume { chatRoom ->
|
||||||
goToChatRoom(chatRoom, true)
|
goToChatRoom(chatRoom, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.updatedChatRoomEvent.observe(
|
viewModel.updatedChatRoomEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { chatRoom ->
|
it.consume { chatRoom ->
|
||||||
goToChatRoom(chatRoom, false)
|
goToChatRoom(chatRoom, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
binding.setNextClickListener {
|
binding.setNextClickListener {
|
||||||
if (viewModel.chatRoom != null) {
|
if (viewModel.chatRoom != null) {
|
||||||
|
@ -182,13 +176,12 @@ class GroupInfoFragment : SecureFragment<ChatRoomGroupInfoFragmentBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.onErrorEvent.observe(
|
viewModel.onErrorEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { messageResourceId ->
|
it.consume { messageResourceId ->
|
||||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addParticipantsFromSharedViewModel() {
|
private fun addParticipantsFromSharedViewModel() {
|
||||||
|
|
|
@ -61,7 +61,7 @@ class ImdnFragment : SecureFragment<ChatRoomImdnFragmentBinding>() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isSecure = chatRoom.currentParams.encryptionEnabled()
|
isSecure = chatRoom.currentParams.isEncryptionEnabled
|
||||||
|
|
||||||
if (arguments != null) {
|
if (arguments != null) {
|
||||||
val messageId = arguments?.getString("MessageId")
|
val messageId = arguments?.getString("MessageId")
|
||||||
|
@ -98,11 +98,10 @@ class ImdnFragment : SecureFragment<ChatRoomImdnFragmentBinding>() {
|
||||||
binding.participantsList.addItemDecoration(headerItemDecoration)
|
binding.participantsList.addItemDecoration(headerItemDecoration)
|
||||||
|
|
||||||
viewModel.participants.observe(
|
viewModel.participants.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
adapter.submitList(it)
|
adapter.submitList(it)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
binding.setBackClickListener {
|
binding.setBackClickListener {
|
||||||
goBack()
|
goBack()
|
||||||
|
|
|
@ -98,7 +98,9 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
|
||||||
isSecure = true
|
isSecure = true
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = viewLifecycleOwner
|
||||||
|
|
||||||
listViewModel = ViewModelProvider(this)[ChatRoomsListViewModel::class.java]
|
listViewModel = requireActivity().run {
|
||||||
|
ViewModelProvider(this)[ChatRoomsListViewModel::class.java]
|
||||||
|
}
|
||||||
binding.viewModel = listViewModel
|
binding.viewModel = listViewModel
|
||||||
|
|
||||||
/* Shared view model & sliding pane related */
|
/* Shared view model & sliding pane related */
|
||||||
|
@ -111,31 +113,31 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
|
||||||
|
|
||||||
// Chat room loading can take some time, so wait until it is ready before opening the pane
|
// Chat room loading can take some time, so wait until it is ready before opening the pane
|
||||||
sharedViewModel.chatRoomFragmentOpenedEvent.observe(
|
sharedViewModel.chatRoomFragmentOpenedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
binding.slidingPane.openPane()
|
binding.slidingPane.openPane()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
sharedViewModel.closeSlidingPaneEvent.observe(
|
sharedViewModel.closeSlidingPaneEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
|
(requireActivity() as MainActivity).hideKeyboard()
|
||||||
if (!binding.slidingPane.closePane()) {
|
if (!binding.slidingPane.closePane()) {
|
||||||
goBack()
|
goBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
sharedViewModel.layoutChangedEvent.observe(
|
sharedViewModel.layoutChangedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
|
sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
|
||||||
if (binding.slidingPane.isSlideable) {
|
if (binding.slidingPane.isSlideable) {
|
||||||
val navHostFragment = childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment
|
val navHostFragment =
|
||||||
|
childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment
|
||||||
if (navHostFragment.navController.currentDestination?.id == R.id.emptyChatFragment) {
|
if (navHostFragment.navController.currentDestination?.id == R.id.emptyChatFragment) {
|
||||||
Log.i("[Chat] Foldable device has been folded, closing side pane with empty fragment")
|
Log.i("[Chat] Foldable device has been folded, closing side pane with empty fragment")
|
||||||
binding.slidingPane.closePane()
|
binding.slidingPane.closePane()
|
||||||
|
@ -143,20 +145,7 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
|
binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
|
||||||
binding.slidingPane.addPanelSlideListener(object : SlidingPaneLayout.PanelSlideListener {
|
|
||||||
override fun onPanelSlide(panel: View, slideOffset: Float) { }
|
|
||||||
|
|
||||||
override fun onPanelOpened(panel: View) { }
|
|
||||||
|
|
||||||
override fun onPanelClosed(panel: View) {
|
|
||||||
if (binding.slidingPane.isSlideable) {
|
|
||||||
// (requireActivity() as MainActivity).showTabsFragment()
|
|
||||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/* End of shared view model & sliding pane related */
|
/* End of shared view model & sliding pane related */
|
||||||
|
|
||||||
|
@ -186,9 +175,9 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
|
||||||
)
|
)
|
||||||
val swipeListener = object : RecyclerViewSwipeListener {
|
val swipeListener = object : RecyclerViewSwipeListener {
|
||||||
override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) {
|
override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) {
|
||||||
val chatRoomViewModel = adapter.currentList[viewHolder.adapterPosition]
|
val chatRoomViewModel = adapter.currentList[viewHolder.bindingAdapterPosition]
|
||||||
chatRoomViewModel.chatRoom.markAsRead()
|
chatRoomViewModel.chatRoom.markAsRead()
|
||||||
adapter.notifyItemChanged(viewHolder.adapterPosition)
|
adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) {
|
override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) {
|
||||||
|
@ -196,13 +185,13 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
|
||||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
|
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
|
||||||
|
|
||||||
viewModel.showCancelButton {
|
viewModel.showCancelButton {
|
||||||
adapter.notifyItemChanged(viewHolder.adapterPosition)
|
adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.showDeleteButton(
|
viewModel.showDeleteButton(
|
||||||
{
|
{
|
||||||
val deletedChatRoom = adapter.currentList[viewHolder.adapterPosition].chatRoom
|
val deletedChatRoom = adapter.currentList[viewHolder.bindingAdapterPosition].chatRoom
|
||||||
listViewModel.deleteChatRoom(deletedChatRoom)
|
listViewModel.deleteChatRoom(deletedChatRoom)
|
||||||
if (!binding.slidingPane.isSlideable &&
|
if (!binding.slidingPane.isSlideable &&
|
||||||
deletedChatRoom == sharedViewModel.selectedChatRoom.value
|
deletedChatRoom == sharedViewModel.selectedChatRoom.value
|
||||||
|
@ -225,31 +214,36 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
|
||||||
binding.chatList.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
|
binding.chatList.addItemDecoration(AppUtils.getDividerDecoration(requireContext(), layoutManager))
|
||||||
|
|
||||||
listViewModel.chatRooms.observe(
|
listViewModel.chatRooms.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{ chatRooms ->
|
) { chatRooms ->
|
||||||
adapter.submitList(chatRooms)
|
adapter.submitList(chatRooms)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
listViewModel.contactsUpdatedEvent.observe(
|
listViewModel.contactsUpdatedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
adapter.selectedChatRoomEvent.observe(
|
adapter.selectedChatRoomEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { chatRoom ->
|
it.consume { chatRoom ->
|
||||||
if ((requireActivity() as GenericActivity).isDestructionPending) {
|
if ((requireActivity() as GenericActivity).isDestructionPending) {
|
||||||
Log.w("[Chat] Activity is pending destruction, don't start navigating now!")
|
Log.w("[Chat] Activity is pending destruction, don't start navigating now!")
|
||||||
sharedViewModel.destructionPendingChatRoom = chatRoom
|
sharedViewModel.destructionPendingChatRoom = chatRoom
|
||||||
} else {
|
} else {
|
||||||
if (chatRoom.peerAddress.asStringUriOnly() == coreContext.notificationsManager.currentlyDisplayedChatRoomAddress) {
|
if (chatRoom.peerAddress.asStringUriOnly() == coreContext.notificationsManager.currentlyDisplayedChatRoomAddress) {
|
||||||
|
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 {
|
||||||
Log.w("[Chat] This chat room is already displayed!")
|
Log.w("[Chat] This chat room is already displayed!")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
sharedViewModel.selectedChatRoom.value = chatRoom
|
sharedViewModel.selectedChatRoom.value = chatRoom
|
||||||
navigateToChatRoom(
|
navigateToChatRoom(
|
||||||
|
@ -261,7 +255,6 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
binding.setEditClickListener {
|
binding.setEditClickListener {
|
||||||
listSelectionViewModel.isEditionEnabled.value = true
|
listSelectionViewModel.isEditionEnabled.value = true
|
||||||
|
@ -315,8 +308,8 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sharedViewModel.textToShare.observe(
|
sharedViewModel.textToShare.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
if (it.isNotEmpty()) {
|
if (it.isNotEmpty()) {
|
||||||
Log.i("[Chat] Found text to share")
|
Log.i("[Chat] Found text to share")
|
||||||
// val activity = requireActivity() as MainActivity
|
// val activity = requireActivity() as MainActivity
|
||||||
|
@ -329,10 +322,9 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
sharedViewModel.filesToShare.observe(
|
sharedViewModel.filesToShare.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
if (it.isNotEmpty()) {
|
if (it.isNotEmpty()) {
|
||||||
Log.i("[Chat] Found ${it.size} files to share")
|
Log.i("[Chat] Found ${it.size} files to share")
|
||||||
// val activity = requireActivity() as MainActivity
|
// val activity = requireActivity() as MainActivity
|
||||||
|
@ -345,26 +337,23 @@ class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, Ch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
sharedViewModel.isPendingMessageForward.observe(
|
sharedViewModel.isPendingMessageForward.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
listViewModel.forwardPending.value = it
|
listViewModel.forwardPending.value = it
|
||||||
adapter.forwardPending(it)
|
adapter.forwardPending(it)
|
||||||
if (it) {
|
if (it) {
|
||||||
Log.i("[Chat] Found chat message to transfer")
|
Log.i("[Chat] Found chat message to transfer")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
listViewModel.onErrorEvent.observe(
|
listViewModel.onErrorEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { messageResourceId ->
|
it.consume { messageResourceId ->
|
||||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
*/
|
*/
|
||||||
package org.linphone.activities.main.chat.viewmodels
|
package org.linphone.activities.main.chat.viewmodels
|
||||||
|
|
||||||
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
@ -37,6 +38,7 @@ import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||||
import org.linphone.R
|
import org.linphone.R
|
||||||
import org.linphone.activities.main.chat.data.ChatMessageAttachmentData
|
import org.linphone.activities.main.chat.data.ChatMessageAttachmentData
|
||||||
import org.linphone.activities.main.chat.data.ChatMessageData
|
import org.linphone.activities.main.chat.data.ChatMessageData
|
||||||
|
import org.linphone.compatibility.Compatibility
|
||||||
import org.linphone.core.*
|
import org.linphone.core.*
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.utils.AppUtils
|
import org.linphone.utils.AppUtils
|
||||||
|
@ -86,11 +88,18 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
|
||||||
|
|
||||||
val isPlayingVoiceRecording = MutableLiveData<Boolean>()
|
val isPlayingVoiceRecording = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
val recorder: Recorder
|
|
||||||
|
|
||||||
val voiceRecordPlayingPosition = MutableLiveData<Int>()
|
val voiceRecordPlayingPosition = MutableLiveData<Int>()
|
||||||
|
|
||||||
var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
|
val imeFlags: Int = if (chatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())) {
|
||||||
|
// IME_FLAG_NO_PERSONALIZED_LEARNING is only available on Android 8 and newer
|
||||||
|
Compatibility.getImeFlagsForSecureChatRoom()
|
||||||
|
} else {
|
||||||
|
EditorInfo.IME_FLAG_NO_EXTRACT_UI
|
||||||
|
}
|
||||||
|
|
||||||
|
private val recorder: Recorder
|
||||||
|
|
||||||
|
private var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
|
||||||
|
|
||||||
private lateinit var voiceRecordingPlayer: Player
|
private lateinit var voiceRecordingPlayer: Player
|
||||||
private val playerListener = PlayerListener {
|
private val playerListener = PlayerListener {
|
||||||
|
@ -98,9 +107,19 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
|
||||||
stopVoiceRecordPlayer()
|
stopVoiceRecordPlayer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() {
|
||||||
|
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||||
|
if (state == ChatRoom.State.Created || state == ChatRoom.State.Terminated) {
|
||||||
|
isReadOnly.value = chatRoom.hasBeenLeft()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
chatRoom.addListener(chatRoomListener)
|
||||||
|
|
||||||
attachments.value = arrayListOf()
|
attachments.value = arrayListOf()
|
||||||
|
|
||||||
attachFileEnabled.value = true
|
attachFileEnabled.value = true
|
||||||
|
@ -118,6 +137,7 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
attachments.value.orEmpty().forEach(ChatMessageAttachmentData::destroy)
|
attachments.value.orEmpty().forEach(ChatMessageAttachmentData::destroy)
|
||||||
|
pendingChatMessageToReplyTo.value?.destroy()
|
||||||
|
|
||||||
if (recorder.state != RecorderState.Closed) {
|
if (recorder.state != RecorderState.Closed) {
|
||||||
recorder.close()
|
recorder.close()
|
||||||
|
@ -128,6 +148,7 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
|
||||||
voiceRecordingPlayer.removeListener(playerListener)
|
voiceRecordingPlayer.removeListener(playerListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chatRoom.removeListener(chatRoomListener)
|
||||||
scope.cancel()
|
scope.cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
@ -418,8 +439,9 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
|
||||||
|
|
||||||
private fun initVoiceRecordPlayer() {
|
private fun initVoiceRecordPlayer() {
|
||||||
Log.i("[Chat Message Sending] Creating player for voice record")
|
Log.i("[Chat Message Sending] Creating player for voice record")
|
||||||
// Use speaker sound card to play recordings, otherwise use earpiece
|
// In case no headphones/headset is connected, use speaker sound card to play recordings, otherwise use earpiece
|
||||||
// If none are available, default one will be used
|
// If none are available, default one will be used
|
||||||
|
var headphonesCard: String? = null
|
||||||
var speakerCard: String? = null
|
var speakerCard: String? = null
|
||||||
var earpieceCard: String? = null
|
var earpieceCard: String? = null
|
||||||
for (device in coreContext.core.audioDevices) {
|
for (device in coreContext.core.audioDevices) {
|
||||||
|
@ -428,12 +450,14 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel()
|
||||||
speakerCard = device.id
|
speakerCard = device.id
|
||||||
} else if (device.type == AudioDevice.Type.Earpiece) {
|
} else if (device.type == AudioDevice.Type.Earpiece) {
|
||||||
earpieceCard = device.id
|
earpieceCard = device.id
|
||||||
|
} else if (device.type == AudioDevice.Type.Headphones || device.type == AudioDevice.Type.Headset) {
|
||||||
|
headphonesCard = device.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.i("[Chat Message Sending] Found speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]")
|
Log.i("[Chat Message Sending] Found headset/headphones sound card [$headphonesCard], speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]")
|
||||||
|
|
||||||
val localPlayer = coreContext.core.createLocalPlayer(speakerCard ?: earpieceCard, null, null)
|
val localPlayer = coreContext.core.createLocalPlayer(headphonesCard ?: speakerCard ?: earpieceCard, null, null)
|
||||||
if (localPlayer != null) {
|
if (localPlayer != null) {
|
||||||
voiceRecordingPlayer = localPlayer
|
voiceRecordingPlayer = localPlayer
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -158,27 +158,21 @@ class ChatMessagesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteMessage(chatMessage: ChatMessage) {
|
fun deleteMessage(chatMessage: ChatMessage) {
|
||||||
val position: Int = chatMessage.userData as Int
|
|
||||||
LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage)
|
LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage)
|
||||||
chatRoom.deleteMessage(chatMessage)
|
chatRoom.deleteMessage(chatMessage)
|
||||||
|
|
||||||
val list = arrayListOf<EventLogData>()
|
events.value.orEmpty().forEach(EventLogData::destroy)
|
||||||
list.addAll(events.value.orEmpty())
|
events.value = getEvents()
|
||||||
list.removeAt(position)
|
|
||||||
events.value = list
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteEventLogs(listToDelete: ArrayList<EventLogData>) {
|
fun deleteEventLogs(listToDelete: ArrayList<EventLogData>) {
|
||||||
val list = arrayListOf<EventLogData>()
|
|
||||||
list.addAll(events.value.orEmpty())
|
|
||||||
|
|
||||||
for (eventLog in listToDelete) {
|
for (eventLog in listToDelete) {
|
||||||
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog.eventLog)
|
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog.eventLog)
|
||||||
eventLog.eventLog.deleteFromDatabase()
|
eventLog.eventLog.deleteFromDatabase()
|
||||||
list.remove(eventLog)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
events.value = list
|
events.value.orEmpty().forEach(EventLogData::destroy)
|
||||||
|
events.value = getEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadMoreData(totalItemsCount: Int) {
|
fun loadMoreData(totalItemsCount: Int) {
|
||||||
|
@ -248,6 +242,8 @@ class ChatMessagesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||||
LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage)
|
LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage)
|
||||||
chatRoom.deleteMessage(chatMessage)
|
chatRoom.deleteMessage(chatMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
events.value.orEmpty().forEach(EventLogData::destroy)
|
||||||
events.value = getEvents()
|
events.value = getEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -160,9 +160,9 @@ class ChatRoomCreationViewModel : ErrorReportingViewModel() {
|
||||||
val encrypted = isEncrypted.value == true
|
val encrypted = isEncrypted.value == true
|
||||||
val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams()
|
val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams()
|
||||||
params.backend = ChatRoomBackend.Basic
|
params.backend = ChatRoomBackend.Basic
|
||||||
params.enableGroup(false)
|
params.isGroupEnabled = false
|
||||||
if (encrypted) {
|
if (encrypted) {
|
||||||
params.enableEncryption(true)
|
params.isEncryptionEnabled = true
|
||||||
params.backend = ChatRoomBackend.FlexisipChat
|
params.backend = ChatRoomBackend.FlexisipChat
|
||||||
params.ephemeralMode = if (corePreferences.useEphemeralPerDeviceMode)
|
params.ephemeralMode = if (corePreferences.useEphemeralPerDeviceMode)
|
||||||
ChatRoomEphemeralMode.DeviceManaged
|
ChatRoomEphemeralMode.DeviceManaged
|
||||||
|
|
|
@ -74,6 +74,8 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
|
||||||
|
|
||||||
val peerSipUri = MutableLiveData<String>()
|
val peerSipUri = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val ephemeralEnabled = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
val oneToOneChatRoom: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())
|
val oneToOneChatRoom: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())
|
||||||
|
|
||||||
val encryptedChatRoom: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())
|
val encryptedChatRoom: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())
|
||||||
|
@ -88,12 +90,12 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
|
||||||
|
|
||||||
var oneParticipantOneDevice: Boolean = false
|
var oneParticipantOneDevice: Boolean = false
|
||||||
|
|
||||||
var addressToCall: Address? = null
|
|
||||||
|
|
||||||
var onlyParticipantOnlyDeviceAddress: Address? = null
|
var onlyParticipantOnlyDeviceAddress: Address? = null
|
||||||
|
|
||||||
val chatUnreadCountTranslateY = MutableLiveData<Float>()
|
val chatUnreadCountTranslateY = MutableLiveData<Float>()
|
||||||
|
|
||||||
|
private var addressToCall: Address? = null
|
||||||
|
|
||||||
private val bounceAnimator: ValueAnimator by lazy {
|
private val bounceAnimator: ValueAnimator by lazy {
|
||||||
ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.tabs_fragment_unread_count_bounce_offset), 0f).apply {
|
ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.tabs_fragment_unread_count_bounce_offset), 0f).apply {
|
||||||
addUpdateListener {
|
addUpdateListener {
|
||||||
|
@ -111,6 +113,7 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
|
||||||
override fun onContactsUpdated() {
|
override fun onContactsUpdated() {
|
||||||
Log.i("[Chat Room] Contacts have changed")
|
Log.i("[Chat Room] Contacts have changed")
|
||||||
contactLookup()
|
contactLookup()
|
||||||
|
updateLastMessageToDisplay()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,7 +199,11 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
|
||||||
|
|
||||||
override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) {
|
override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||||
Log.i("[Chat Room] Ephemeral message deleted, updated last message displayed")
|
Log.i("[Chat Room] Ephemeral message deleted, updated last message displayed")
|
||||||
lastMessageText.value = formatLastMessage(chatRoom.lastMessageInHistory)
|
updateLastMessageToDisplay()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEphemeralEvent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||||
|
ephemeralEnabled.value = chatRoom.isEphemeralEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||||
|
@ -209,16 +216,17 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
|
||||||
chatRoom.addListener(chatRoomListener)
|
chatRoom.addListener(chatRoomListener)
|
||||||
coreContext.contactsManager.addListener(contactsUpdatedListener)
|
coreContext.contactsManager.addListener(contactsUpdatedListener)
|
||||||
|
|
||||||
lastMessageText.value = formatLastMessage(chatRoom.lastMessageInHistory)
|
|
||||||
unreadMessagesCount.value = chatRoom.unreadMessagesCount
|
unreadMessagesCount.value = chatRoom.unreadMessagesCount
|
||||||
lastUpdate.value = TimestampUtils.toString(chatRoom.lastUpdateTime, true)
|
lastUpdate.value = TimestampUtils.toString(chatRoom.lastUpdateTime, true)
|
||||||
|
|
||||||
subject.value = chatRoom.subject
|
subject.value = chatRoom.subject
|
||||||
updateSecurityIcon()
|
updateSecurityIcon()
|
||||||
meAdmin.value = chatRoom.me?.isAdmin ?: false
|
meAdmin.value = chatRoom.me?.isAdmin ?: false
|
||||||
|
ephemeralEnabled.value = chatRoom.isEphemeralEnabled
|
||||||
|
|
||||||
contactLookup()
|
contactLookup()
|
||||||
updateParticipants()
|
updateParticipants()
|
||||||
|
updateLastMessageToDisplay()
|
||||||
|
|
||||||
callInProgress.value = chatRoom.core.callsNb > 0
|
callInProgress.value = chatRoom.core.callsNb > 0
|
||||||
updateRemotesComposing()
|
updateRemotesComposing()
|
||||||
|
@ -268,6 +276,10 @@ class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateLastMessageToDisplay() {
|
||||||
|
lastMessageText.value = formatLastMessage(chatRoom.lastMessageInHistory)
|
||||||
|
}
|
||||||
|
|
||||||
private fun formatLastMessage(msg: ChatMessage?): String {
|
private fun formatLastMessage(msg: ChatMessage?): String {
|
||||||
if (msg == null) return ""
|
if (msg == null) return ""
|
||||||
|
|
||||||
|
|
|
@ -142,9 +142,7 @@ class ChatRoomsListViewModel : ErrorReportingViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateChatRooms() {
|
private fun updateChatRooms() {
|
||||||
for (chatRoomViewModel in chatRooms.value.orEmpty()) {
|
chatRooms.value.orEmpty().forEach(ChatRoomViewModel::destroy)
|
||||||
chatRoomViewModel.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
val list = arrayListOf<ChatRoomViewModel>()
|
val list = arrayListOf<ChatRoomViewModel>()
|
||||||
for (chatRoom in coreContext.core.chatRooms) {
|
for (chatRoom in coreContext.core.chatRooms) {
|
||||||
|
@ -155,6 +153,14 @@ class ChatRoomsListViewModel : ErrorReportingViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addChatRoom(chatRoom: ChatRoom) {
|
private fun addChatRoom(chatRoom: ChatRoom) {
|
||||||
|
val exists = chatRooms.value.orEmpty().find {
|
||||||
|
it.chatRoom.localAddress.weakEqual(chatRoom.localAddress) && it.chatRoom.peerAddress.weakEqual(chatRoom.peerAddress)
|
||||||
|
}
|
||||||
|
if (exists != null) {
|
||||||
|
Log.w("[Chat Rooms] Do not add chat room to list, it's already here")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val list = arrayListOf<ChatRoomViewModel>()
|
val list = arrayListOf<ChatRoomViewModel>()
|
||||||
val viewModel = ChatRoomViewModel(chatRoom)
|
val viewModel = ChatRoomViewModel(chatRoom)
|
||||||
list.add(viewModel)
|
list.add(viewModel)
|
||||||
|
@ -170,12 +176,10 @@ class ChatRoomsListViewModel : ErrorReportingViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findChatRoomIndex(chatRoom: ChatRoom): Int {
|
private fun findChatRoomIndex(chatRoom: ChatRoom): Int {
|
||||||
var index = 0
|
for ((index, chatRoomViewModel) in chatRooms.value.orEmpty().withIndex()) {
|
||||||
for (chatRoomViewModel in chatRooms.value.orEmpty()) {
|
|
||||||
if (chatRoomViewModel.chatRoom == chatRoom) {
|
if (chatRoomViewModel.chatRoom == chatRoom) {
|
||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
index++
|
|
||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,16 +59,16 @@ class DevicesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
chatRoom.addListener(listener)
|
chatRoom.addListener(listener)
|
||||||
updateParticipants()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
participants.value.orEmpty().forEach(DevicesListGroupData::destroy)
|
participants.value.orEmpty().forEach(DevicesListGroupData::destroy)
|
||||||
chatRoom.removeListener(listener)
|
chatRoom.removeListener(listener)
|
||||||
|
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateParticipants() {
|
fun updateParticipants() {
|
||||||
participants.value.orEmpty().forEach(DevicesListGroupData::destroy)
|
participants.value.orEmpty().forEach(DevicesListGroupData::destroy)
|
||||||
|
|
||||||
val list = arrayListOf<DevicesListGroupData>()
|
val list = arrayListOf<DevicesListGroupData>()
|
||||||
|
|
|
@ -50,8 +50,8 @@ class EphemeralViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Log.i("[Ephemeral Messages] Current lifetime is ${chatRoom.ephemeralLifetime}, ephemeral enabled? ${chatRoom.ephemeralEnabled()}")
|
Log.i("[Ephemeral Messages] Current lifetime is ${chatRoom.ephemeralLifetime}, ephemeral enabled? ${chatRoom.isEphemeralEnabled}")
|
||||||
currentSelectedDuration = if (chatRoom.ephemeralEnabled()) chatRoom.ephemeralLifetime else 0
|
currentSelectedDuration = if (chatRoom.isEphemeralEnabled) chatRoom.ephemeralLifetime else 0
|
||||||
computeEphemeralDurationValues()
|
computeEphemeralDurationValues()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,13 +65,13 @@ class EphemeralViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||||
Log.i("[Ephemeral Messages] Configured lifetime for ephemeral messages was already $currentSelectedDuration")
|
Log.i("[Ephemeral Messages] Configured lifetime for ephemeral messages was already $currentSelectedDuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!chatRoom.ephemeralEnabled()) {
|
if (!chatRoom.isEphemeralEnabled) {
|
||||||
Log.i("[Ephemeral Messages] Ephemeral messages were disabled, enable them")
|
Log.i("[Ephemeral Messages] Ephemeral messages were disabled, enable them")
|
||||||
chatRoom.enableEphemeral(true)
|
chatRoom.isEphemeralEnabled = true
|
||||||
}
|
}
|
||||||
} else if (chatRoom.ephemeralEnabled()) {
|
} else if (chatRoom.isEphemeralEnabled) {
|
||||||
Log.i("[Ephemeral Messages] Ephemeral messages were enabled, disable them")
|
Log.i("[Ephemeral Messages] Ephemeral messages were enabled, disable them")
|
||||||
chatRoom.enableEphemeral(false)
|
chatRoom.isEphemeralEnabled = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -117,8 +117,8 @@ class GroupInfoViewModel(val chatRoom: ChatRoom?) : ErrorReportingViewModel() {
|
||||||
fun createChatRoom() {
|
fun createChatRoom() {
|
||||||
waitForChatRoomCreation.value = true
|
waitForChatRoomCreation.value = true
|
||||||
val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams()
|
val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams()
|
||||||
params.enableEncryption(isEncrypted.value == true)
|
params.isEncryptionEnabled = isEncrypted.value == true
|
||||||
params.enableGroup(true)
|
params.isGroupEnabled = true
|
||||||
if (isEncrypted.value == true) {
|
if (isEncrypted.value == true) {
|
||||||
params.ephemeralMode = if (corePreferences.useEphemeralPerDeviceMode)
|
params.ephemeralMode = if (corePreferences.useEphemeralPerDeviceMode)
|
||||||
ChatRoomEphemeralMode.DeviceManaged
|
ChatRoomEphemeralMode.DeviceManaged
|
||||||
|
|
|
@ -21,6 +21,7 @@ package org.linphone.activities.main.chat.views
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.Layout
|
import android.text.Layout
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import androidx.appcompat.widget.AppCompatTextView
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
|
@ -40,6 +41,12 @@ class MultiLineWrapContentWidthTextView : AppCompatTextView {
|
||||||
defStyleAttr: Int
|
defStyleAttr: Int
|
||||||
) : super(context, attrs, defStyleAttr)
|
) : super(context, attrs, defStyleAttr)
|
||||||
|
|
||||||
|
override fun setText(text: CharSequence?, type: BufferType?) {
|
||||||
|
super.setText(text, type)
|
||||||
|
// Required for PatternClickableSpan
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
||||||
var wSpec = widthSpec
|
var wSpec = widthSpec
|
||||||
val widthMode = MeasureSpec.getMode(wSpec)
|
val widthMode = MeasureSpec.getMode(wSpec)
|
||||||
|
|
|
@ -22,6 +22,7 @@ package org.linphone.activities.main.chat.views
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.view.KeyEvent
|
||||||
import androidx.appcompat.widget.AppCompatEditText
|
import androidx.appcompat.widget.AppCompatEditText
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
@ -35,6 +36,10 @@ import org.linphone.utils.Event
|
||||||
* Allows for image input inside an EditText, usefull for keyboards with gif support for example.
|
* Allows for image input inside an EditText, usefull for keyboards with gif support for example.
|
||||||
*/
|
*/
|
||||||
class RichEditText : AppCompatEditText {
|
class RichEditText : AppCompatEditText {
|
||||||
|
private var controlPressed = false
|
||||||
|
|
||||||
|
private var sendListener: RichEditTextSendListener? = null
|
||||||
|
|
||||||
constructor(context: Context) : super(context) {
|
constructor(context: Context) : super(context) {
|
||||||
initReceiveContentListener()
|
initReceiveContentListener()
|
||||||
}
|
}
|
||||||
|
@ -51,6 +56,10 @@ class RichEditText : AppCompatEditText {
|
||||||
initReceiveContentListener()
|
initReceiveContentListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setControlEnterListener(listener: RichEditTextSendListener) {
|
||||||
|
sendListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
private fun initReceiveContentListener() {
|
private fun initReceiveContentListener() {
|
||||||
ViewCompat.setOnReceiveContentListener(
|
ViewCompat.setOnReceiveContentListener(
|
||||||
this, RichContentReceiver.MIME_TYPES,
|
this, RichContentReceiver.MIME_TYPES,
|
||||||
|
@ -63,5 +72,25 @@ class RichEditText : AppCompatEditText {
|
||||||
sharedViewModel.richContentUri.value = Event(uri)
|
sharedViewModel.richContentUri.value = Event(uri)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
setOnKeyListener { _, keyCode, event ->
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_CTRL_LEFT) {
|
||||||
|
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||||
|
controlPressed = true
|
||||||
|
} else if (event.action == KeyEvent.ACTION_UP) {
|
||||||
|
controlPressed = false
|
||||||
|
}
|
||||||
|
false
|
||||||
|
} else if (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_UP && controlPressed) {
|
||||||
|
sendListener?.onControlEnterPressedAndReleased()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RichEditTextSendListener {
|
||||||
|
fun onControlEnterPressedAndReleased()
|
||||||
|
}
|
||||||
|
|
|
@ -71,15 +71,14 @@ class ContactsListAdapter(
|
||||||
// This is for item selection through ListTopBarFragment
|
// This is for item selection through ListTopBarFragment
|
||||||
selectionListViewModel = selectionViewModel
|
selectionListViewModel = selectionViewModel
|
||||||
selectionViewModel.isEditionEnabled.observe(
|
selectionViewModel.isEditionEnabled.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
position = adapterPosition
|
position = bindingAdapterPosition
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
setClickListener {
|
setClickListener {
|
||||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||||
selectionViewModel.onToggleSelect(adapterPosition)
|
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||||
} else {
|
} else {
|
||||||
selectedContactEvent.value = Event(contactViewModel.contactInternal)
|
selectedContactEvent.value = Event(contactViewModel.contactInternal)
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,47 +83,50 @@ class DetailContactFragment : GenericFragment<ContactDetailFragmentBinding>() {
|
||||||
binding.viewModel = viewModel
|
binding.viewModel = viewModel
|
||||||
|
|
||||||
viewModel.sendSmsToEvent.observe(
|
viewModel.sendSmsToEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { number ->
|
it.consume { number ->
|
||||||
sendSms(number)
|
sendSms(number)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.startCallToEvent.observe(
|
viewModel.startCallToEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { address ->
|
it.consume { address ->
|
||||||
if (coreContext.core.callsNb > 0) {
|
if (coreContext.core.callsNb > 0) {
|
||||||
Log.i("[Contact] Starting dialer with pre-filled URI ${address.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}")
|
Log.i("[Contact] Starting dialer with pre-filled URI ${address.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}")
|
||||||
sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event(R.id.dialerFragment)
|
sharedViewModel.updateContactsAnimationsBasedOnDestination.value =
|
||||||
sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.masterContactsFragment)
|
Event(R.id.dialerFragment)
|
||||||
|
sharedViewModel.updateDialerAnimationsBasedOnDestination.value =
|
||||||
|
Event(R.id.masterContactsFragment)
|
||||||
|
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
args.putString("URI", address.asStringUriOnly())
|
args.putString("URI", address.asStringUriOnly())
|
||||||
args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer)
|
args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer)
|
||||||
args.putBoolean("SkipAutoCallStart", true) // If auto start call setting is enabled, ignore it
|
args.putBoolean(
|
||||||
|
"SkipAutoCallStart",
|
||||||
|
true
|
||||||
|
) // If auto start call setting is enabled, ignore it
|
||||||
navigateToDialer(args)
|
navigateToDialer(args)
|
||||||
} else {
|
} else {
|
||||||
coreContext.startCall(address)
|
coreContext.startCall(address)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.chatRoomCreatedEvent.observe(
|
viewModel.chatRoomCreatedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { chatRoom ->
|
it.consume { chatRoom ->
|
||||||
sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event(R.id.masterChatRoomsFragment)
|
sharedViewModel.updateContactsAnimationsBasedOnDestination.value =
|
||||||
|
Event(R.id.masterChatRoomsFragment)
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
args.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly())
|
args.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly())
|
||||||
args.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly())
|
args.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly())
|
||||||
navigateToChatRoom(args)
|
navigateToChatRoom(args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
binding.setBackClickListener {
|
binding.setBackClickListener {
|
||||||
goBack()
|
goBack()
|
||||||
|
@ -138,13 +141,12 @@ class DetailContactFragment : GenericFragment<ContactDetailFragmentBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.onErrorEvent.observe(
|
viewModel.onErrorEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { messageResourceId ->
|
it.consume { messageResourceId ->
|
||||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
view.doOnPreDraw {
|
view.doOnPreDraw {
|
||||||
// Notifies fragment is ready to be drawn
|
// Notifies fragment is ready to be drawn
|
||||||
|
|
|
@ -85,16 +85,18 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
|
||||||
|
|
||||||
useMaterialSharedAxisXForwardAnimation = false
|
useMaterialSharedAxisXForwardAnimation = false
|
||||||
sharedViewModel.updateContactsAnimationsBasedOnDestination.observe(
|
sharedViewModel.updateContactsAnimationsBasedOnDestination.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { id ->
|
it.consume { id ->
|
||||||
val forward = when (id) {
|
val forward = when (id) {
|
||||||
R.id.dialerFragment, R.id.masterChatRoomsFragment -> false
|
R.id.dialerFragment, R.id.masterChatRoomsFragment -> false
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
if (corePreferences.enableAnimations) {
|
if (corePreferences.enableAnimations) {
|
||||||
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
|
val portraitOrientation =
|
||||||
val axis = if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y
|
resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
val axis =
|
||||||
|
if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y
|
||||||
enterTransition = MaterialSharedAxis(axis, forward)
|
enterTransition = MaterialSharedAxis(axis, forward)
|
||||||
reenterTransition = MaterialSharedAxis(axis, forward)
|
reenterTransition = MaterialSharedAxis(axis, forward)
|
||||||
returnTransition = MaterialSharedAxis(axis, !forward)
|
returnTransition = MaterialSharedAxis(axis, !forward)
|
||||||
|
@ -102,34 +104,32 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
sharedViewModel.contactFragmentOpenedEvent.observe(
|
sharedViewModel.contactFragmentOpenedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
binding.slidingPane.openPane()
|
binding.slidingPane.openPane()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
sharedViewModel.closeSlidingPaneEvent.observe(
|
sharedViewModel.closeSlidingPaneEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
if (!binding.slidingPane.closePane()) {
|
if (!binding.slidingPane.closePane()) {
|
||||||
goBack()
|
goBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
sharedViewModel.layoutChangedEvent.observe(
|
sharedViewModel.layoutChangedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
|
sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
|
||||||
if (binding.slidingPane.isSlideable) {
|
if (binding.slidingPane.isSlideable) {
|
||||||
val navHostFragment = childFragmentManager.findFragmentById(R.id.contacts_nav_container) as NavHostFragment
|
val navHostFragment =
|
||||||
|
childFragmentManager.findFragmentById(R.id.contacts_nav_container) as NavHostFragment
|
||||||
if (navHostFragment.navController.currentDestination?.id == R.id.emptyContactFragment) {
|
if (navHostFragment.navController.currentDestination?.id == R.id.emptyContactFragment) {
|
||||||
Log.i("[Contacts] Foldable device has been folded, closing side pane with empty fragment")
|
Log.i("[Contacts] Foldable device has been folded, closing side pane with empty fragment")
|
||||||
binding.slidingPane.closePane()
|
binding.slidingPane.closePane()
|
||||||
|
@ -137,23 +137,7 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
|
binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
|
||||||
/*binding.slidingPane.addPanelSlideListener(object : SlidingPaneLayout.PanelSlideListener {
|
|
||||||
override fun onPanelSlide(panel: View, slideOffset: Float) { }
|
|
||||||
|
|
||||||
override fun onPanelOpened(panel: View) {
|
|
||||||
if (binding.slidingPane.isSlideable) {
|
|
||||||
(requireActivity() as MainActivity).hideTabsFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPanelClosed(panel: View) {
|
|
||||||
if (binding.slidingPane.isSlideable) {
|
|
||||||
(requireActivity() as MainActivity).showTabsFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})*/
|
|
||||||
|
|
||||||
/* End of shared view model & sliding pane related */
|
/* End of shared view model & sliding pane related */
|
||||||
|
|
||||||
|
@ -190,13 +174,13 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
|
||||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
|
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
|
||||||
|
|
||||||
viewModel.showCancelButton {
|
viewModel.showCancelButton {
|
||||||
adapter.notifyItemChanged(viewHolder.adapterPosition)
|
adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.showDeleteButton(
|
viewModel.showDeleteButton(
|
||||||
{
|
{
|
||||||
val deletedContact = adapter.currentList[viewHolder.adapterPosition].contactInternal
|
val deletedContact = adapter.currentList[viewHolder.bindingAdapterPosition].contactInternal
|
||||||
listViewModel.deleteContact(deletedContact)
|
listViewModel.deleteContact(deletedContact)
|
||||||
if (!binding.slidingPane.isSlideable &&
|
if (!binding.slidingPane.isSlideable &&
|
||||||
deletedContact == sharedViewModel.selectedContact.value
|
deletedContact == sharedViewModel.selectedContact.value
|
||||||
|
@ -223,8 +207,8 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
|
||||||
binding.contactsList.addItemDecoration(headerItemDecoration)
|
binding.contactsList.addItemDecoration(headerItemDecoration)
|
||||||
|
|
||||||
adapter.selectedContactEvent.observe(
|
adapter.selectedContactEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { contact ->
|
it.consume { contact ->
|
||||||
Log.i("[Contacts] Selected item in list changed: $contact")
|
Log.i("[Contacts] Selected item in list changed: $contact")
|
||||||
sharedViewModel.selectedContact.value = contact
|
sharedViewModel.selectedContact.value = contact
|
||||||
|
@ -238,11 +222,10 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
listViewModel.contactsList.observe(
|
listViewModel.contactsList.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
val id = contactIdToDisplay
|
val id = contactIdToDisplay
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
val contact = coreContext.contactsManager.findContactById(id)
|
val contact = coreContext.contactsManager.findContactById(id)
|
||||||
|
@ -254,7 +237,6 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
|
||||||
}
|
}
|
||||||
adapter.submitList(it)
|
adapter.submitList(it)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
binding.setAllContactsToggleClickListener {
|
binding.setAllContactsToggleClickListener {
|
||||||
listViewModel.sipContactsSelected.value = false
|
listViewModel.sipContactsSelected.value = false
|
||||||
|
@ -264,18 +246,16 @@ class MasterContactsFragment : MasterFragment<ContactMasterFragmentBinding, Cont
|
||||||
}
|
}
|
||||||
|
|
||||||
listViewModel.sipContactsSelected.observe(
|
listViewModel.sipContactsSelected.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
listViewModel.updateContactsList()
|
listViewModel.updateContactsList()
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
listViewModel.filter.observe(
|
listViewModel.filter.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
listViewModel.updateContactsList()
|
listViewModel.updateContactsList()
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
binding.setNewContactClickListener {
|
binding.setNewContactClickListener {
|
||||||
// Remove any previously selected contact
|
// Remove any previously selected contact
|
||||||
|
|
|
@ -45,9 +45,11 @@ import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||||
import org.linphone.activities.navigateToConfigFileViewer
|
import org.linphone.activities.navigateToConfigFileViewer
|
||||||
import org.linphone.activities.navigateToContacts
|
import org.linphone.activities.navigateToContacts
|
||||||
|
import org.linphone.compatibility.Compatibility
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.databinding.DialerFragmentBinding
|
import org.linphone.databinding.DialerFragmentBinding
|
||||||
import org.linphone.mediastream.Version
|
import org.linphone.mediastream.Version
|
||||||
|
import org.linphone.telecom.TelecomHelper
|
||||||
import org.linphone.utils.AppUtils
|
import org.linphone.utils.AppUtils
|
||||||
import org.linphone.utils.DialogUtils
|
import org.linphone.utils.DialogUtils
|
||||||
import org.linphone.utils.Event
|
import org.linphone.utils.Event
|
||||||
|
@ -75,16 +77,18 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
|
||||||
|
|
||||||
useMaterialSharedAxisXForwardAnimation = false
|
useMaterialSharedAxisXForwardAnimation = false
|
||||||
sharedViewModel.updateDialerAnimationsBasedOnDestination.observe(
|
sharedViewModel.updateDialerAnimationsBasedOnDestination.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { id ->
|
it.consume { id ->
|
||||||
val forward = when (id) {
|
val forward = when (id) {
|
||||||
R.id.masterChatRoomsFragment -> false
|
R.id.masterChatRoomsFragment -> false
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
if (corePreferences.enableAnimations) {
|
if (corePreferences.enableAnimations) {
|
||||||
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
|
val portraitOrientation =
|
||||||
val axis = if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y
|
resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
val axis =
|
||||||
|
if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y
|
||||||
enterTransition = MaterialSharedAxis(axis, forward)
|
enterTransition = MaterialSharedAxis(axis, forward)
|
||||||
reenterTransition = MaterialSharedAxis(axis, forward)
|
reenterTransition = MaterialSharedAxis(axis, forward)
|
||||||
returnTransition = MaterialSharedAxis(axis, !forward)
|
returnTransition = MaterialSharedAxis(axis, !forward)
|
||||||
|
@ -92,7 +96,6 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
binding.setNewContactClickListener {
|
binding.setNewContactClickListener {
|
||||||
sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.masterContactsFragment)
|
sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.masterContactsFragment)
|
||||||
|
@ -108,6 +111,47 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.enteredUri.observe(
|
||||||
|
viewLifecycleOwner
|
||||||
|
) {
|
||||||
|
if (it == corePreferences.debugPopupCode) {
|
||||||
|
displayDebugPopup()
|
||||||
|
viewModel.enteredUri.value = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.uploadFinishedEvent.observe(
|
||||||
|
viewLifecycleOwner
|
||||||
|
) {
|
||||||
|
it.consume { url ->
|
||||||
|
// To prevent being trigger when using the Send Logs button in About page
|
||||||
|
if (uploadLogsInitiatedByUs) {
|
||||||
|
val clipboard =
|
||||||
|
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip = ClipData.newPlainText("Logs url", url)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
|
||||||
|
val activity = requireActivity() as MainActivity
|
||||||
|
activity.showSnackBar(R.string.logs_url_copied_to_clipboard)
|
||||||
|
|
||||||
|
AppUtils.shareUploadedLogsUrl(activity, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.updateAvailableEvent.observe(
|
||||||
|
viewLifecycleOwner
|
||||||
|
) {
|
||||||
|
it.consume { url ->
|
||||||
|
displayNewVersionAvailableDialog(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (corePreferences.firstStart) {
|
||||||
|
Log.w("[Dialer] First start detected, wait for assistant to be finished to check for update & request permissions")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (arguments?.containsKey("Transfer") == true) {
|
if (arguments?.containsKey("Transfer") == true) {
|
||||||
sharedViewModel.pendingCallTransfer = arguments?.getBoolean("Transfer") ?: false
|
sharedViewModel.pendingCallTransfer = arguments?.getBoolean("Transfer") ?: false
|
||||||
Log.i("[Dialer] Is pending call transfer: ${sharedViewModel.pendingCallTransfer}")
|
Log.i("[Dialer] Is pending call transfer: ${sharedViewModel.pendingCallTransfer}")
|
||||||
|
@ -127,45 +171,6 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
|
||||||
}
|
}
|
||||||
arguments?.clear()
|
arguments?.clear()
|
||||||
|
|
||||||
viewModel.enteredUri.observe(
|
|
||||||
viewLifecycleOwner,
|
|
||||||
{
|
|
||||||
if (it == corePreferences.debugPopupCode) {
|
|
||||||
displayDebugPopup()
|
|
||||||
viewModel.enteredUri.value = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.uploadFinishedEvent.observe(
|
|
||||||
viewLifecycleOwner,
|
|
||||||
{
|
|
||||||
it.consume { url ->
|
|
||||||
// To prevent being trigger when using the Send Logs button in About page
|
|
||||||
if (uploadLogsInitiatedByUs) {
|
|
||||||
val clipboard =
|
|
||||||
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
val clip = ClipData.newPlainText("Logs url", url)
|
|
||||||
clipboard.setPrimaryClip(clip)
|
|
||||||
|
|
||||||
val activity = requireActivity() as MainActivity
|
|
||||||
activity.showSnackBar(R.string.logs_url_copied_to_clipboard)
|
|
||||||
|
|
||||||
AppUtils.shareUploadedLogsUrl(activity, url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.updateAvailableEvent.observe(
|
|
||||||
viewLifecycleOwner,
|
|
||||||
{
|
|
||||||
it.consume { url ->
|
|
||||||
displayNewVersionAvailableDialog(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
Log.i("[Dialer] Pending call transfer mode = ${sharedViewModel.pendingCallTransfer}")
|
Log.i("[Dialer] Pending call transfer mode = ${sharedViewModel.pendingCallTransfer}")
|
||||||
viewModel.transferVisibility.value = sharedViewModel.pendingCallTransfer
|
viewModel.transferVisibility.value = sharedViewModel.pendingCallTransfer
|
||||||
|
|
||||||
|
@ -205,18 +210,72 @@ class DialerFragment : SecureFragment<DialerFragmentBinding>() {
|
||||||
Log.i("[Dialer] READ_PHONE_STATE permission has been granted")
|
Log.i("[Dialer] READ_PHONE_STATE permission has been granted")
|
||||||
coreContext.initPhoneStateListener()
|
coreContext.initPhoneStateListener()
|
||||||
}
|
}
|
||||||
|
checkTelecomManagerPermissions()
|
||||||
|
} else if (requestCode == 1) {
|
||||||
|
var allGranted = true
|
||||||
|
for (result in grantResults) {
|
||||||
|
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
allGranted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allGranted) {
|
||||||
|
Log.i("[Dialer] Telecom Manager permission have been granted")
|
||||||
|
enableTelecomManager()
|
||||||
|
} else {
|
||||||
|
Log.w("[Dialer] Telecom Manager permission have been denied (at least one of them)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Version.API23_MARSHMALLOW_60)
|
@TargetApi(Version.API23_MARSHMALLOW_60)
|
||||||
private fun checkPermissions() {
|
private fun checkPermissions() {
|
||||||
|
checkReadPhoneStatePermission()
|
||||||
|
if (Version.sdkAboveOrEqual(Version.API26_O_80) && PermissionHelper.get().hasReadPhoneStatePermission()) {
|
||||||
|
// Don't check the following the previous permission is being asked
|
||||||
|
checkTelecomManagerPermissions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Version.API23_MARSHMALLOW_60)
|
||||||
|
private fun checkReadPhoneStatePermission() {
|
||||||
if (!PermissionHelper.get().hasReadPhoneStatePermission()) {
|
if (!PermissionHelper.get().hasReadPhoneStatePermission()) {
|
||||||
Log.i("[Dialer] Asking for READ_PHONE_STATE permission")
|
Log.i("[Dialer] Asking for READ_PHONE_STATE permission")
|
||||||
requestPermissions(arrayOf(Manifest.permission.READ_PHONE_STATE), 0)
|
requestPermissions(arrayOf(Manifest.permission.READ_PHONE_STATE), 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TargetApi(Version.API26_O_80)
|
||||||
|
private fun checkTelecomManagerPermissions() {
|
||||||
|
if (!corePreferences.useTelecomManager) {
|
||||||
|
Log.i("[Dialer] Telecom Manager feature is disabled")
|
||||||
|
if (corePreferences.manuallyDisabledTelecomManager) {
|
||||||
|
Log.w("[Dialer] User has manually disabled Telecom Manager feature")
|
||||||
|
} else {
|
||||||
|
if (Compatibility.hasTelecomManagerPermissions(requireContext())) {
|
||||||
|
enableTelecomManager()
|
||||||
|
} else {
|
||||||
|
Log.i("[Dialer] Asking for Telecom Manager permissions")
|
||||||
|
Compatibility.requestTelecomManagerPermissions(requireActivity(), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.i("[Dialer] Telecom Manager feature is already enabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Version.API26_O_80)
|
||||||
|
private fun enableTelecomManager() {
|
||||||
|
Log.i("[Dialer] Telecom Manager permissions granted")
|
||||||
|
if (!TelecomHelper.exists()) {
|
||||||
|
Log.i("[Dialer] Creating Telecom Helper")
|
||||||
|
TelecomHelper.create(requireContext())
|
||||||
|
} else {
|
||||||
|
Log.e("[Dialer] Telecom Manager was already created ?!")
|
||||||
|
}
|
||||||
|
corePreferences.useTelecomManager = true
|
||||||
|
}
|
||||||
|
|
||||||
private fun displayDebugPopup() {
|
private fun displayDebugPopup() {
|
||||||
val alertDialog = MaterialAlertDialogBuilder(requireContext())
|
val alertDialog = MaterialAlertDialogBuilder(requireContext())
|
||||||
alertDialog.setTitle(getString(R.string.debug_popup_title))
|
alertDialog.setTitle(getString(R.string.debug_popup_title))
|
||||||
|
|
|
@ -21,7 +21,6 @@ package org.linphone.activities.main.dialer.viewmodels
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Vibrator
|
import android.os.Vibrator
|
||||||
import android.provider.Settings
|
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
@ -69,25 +68,10 @@ class DialerViewModel : LogsUploadViewModel() {
|
||||||
}
|
}
|
||||||
enteredUri.value = sb.toString()
|
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) {
|
if (vibrator.hasVibrator() && corePreferences.dtmfKeypadVibration) {
|
||||||
Compatibility.eventVibration(vibrator)
|
Compatibility.eventVibration(vibrator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (snfe: Settings.SettingNotFoundException) {
|
|
||||||
Log.e("[Dialer] Can't play DTMF: $snfe")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleLongClick(key: Char): Boolean {
|
override fun handleLongClick(key: Char): Boolean {
|
||||||
if (key == '1') {
|
if (key == '1') {
|
||||||
|
@ -176,7 +160,7 @@ class DialerViewModel : LogsUploadViewModel() {
|
||||||
fun updateShowVideoPreview() {
|
fun updateShowVideoPreview() {
|
||||||
val videoPreview = corePreferences.videoPreview
|
val videoPreview = corePreferences.videoPreview
|
||||||
showPreview.value = videoPreview
|
showPreview.value = videoPreview
|
||||||
coreContext.core.enableVideoPreview(videoPreview)
|
coreContext.core.isVideoPreviewEnabled = videoPreview
|
||||||
}
|
}
|
||||||
|
|
||||||
fun eraseLastChar() {
|
fun eraseLastChar() {
|
||||||
|
|
|
@ -56,4 +56,8 @@ abstract class GenericViewerFragment<T : ViewDataBinding> : SecureFragment<T>()
|
||||||
(childFragmentManager.findFragmentById(R.id.top_bar_fragment) as? TopBarFragment)
|
(childFragmentManager.findFragmentById(R.id.top_bar_fragment) as? TopBarFragment)
|
||||||
?.setContent(content)
|
?.setContent(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun goBack() {
|
||||||
|
findNavController().popBackStack()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,13 +21,21 @@ package org.linphone.activities.main.files.fragments
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.linphone.R
|
import org.linphone.R
|
||||||
import org.linphone.activities.GenericFragment
|
import org.linphone.activities.GenericFragment
|
||||||
import org.linphone.activities.SnackBarActivity
|
import org.linphone.activities.SnackBarActivity
|
||||||
|
import org.linphone.compatibility.Compatibility
|
||||||
import org.linphone.core.Content
|
import org.linphone.core.Content
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.databinding.FileViewerTopBarFragmentBinding
|
import org.linphone.databinding.FileViewerTopBarFragmentBinding
|
||||||
|
import org.linphone.mediastream.Version
|
||||||
import org.linphone.utils.FileUtils
|
import org.linphone.utils.FileUtils
|
||||||
|
import org.linphone.utils.PermissionHelper
|
||||||
|
|
||||||
class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
|
class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
|
||||||
private var content: Content? = null
|
private var content: Content? = null
|
||||||
|
@ -46,20 +54,9 @@ class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.setExportClickListener {
|
binding.setExportClickListener {
|
||||||
if (content != null) {
|
val contentToExport = content
|
||||||
val filePath = content?.plainFilePath.orEmpty()
|
if (contentToExport != null) {
|
||||||
plainFilePath = if (filePath.isEmpty()) content?.filePath.orEmpty() else filePath
|
exportContent(contentToExport)
|
||||||
Log.i("[File Viewer] Plain file path is: $plainFilePath")
|
|
||||||
if (plainFilePath.isNotEmpty()) {
|
|
||||||
if (!FileUtils.openFileInThirdPartyApp(requireActivity(), plainFilePath)) {
|
|
||||||
(requireActivity() as SnackBarActivity).showSnackBar(R.string.chat_message_no_app_found_to_handle_file_mime_type)
|
|
||||||
if (plainFilePath != content?.filePath.orEmpty()) {
|
|
||||||
Log.i("[File Viewer] No app to open plain file path: $plainFilePath, destroying it")
|
|
||||||
FileUtils.deleteFile(plainFilePath)
|
|
||||||
}
|
|
||||||
plainFilePath = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Log.e("[File Viewer] No Content set!")
|
Log.e("[File Viewer] No Content set!")
|
||||||
}
|
}
|
||||||
|
@ -89,4 +86,77 @@ class TopBarFragment : GenericFragment<FileViewerTopBarFragmentBinding>() {
|
||||||
content = c
|
content = c
|
||||||
binding.fileName.text = c.name
|
binding.fileName.text = c.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun exportContent(content: Content) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
var mediaStoreFilePath = ""
|
||||||
|
if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10) || PermissionHelper.get().hasWriteExternalStoragePermission()) {
|
||||||
|
Log.i("[File Viewer] Exporting image through Media Store API")
|
||||||
|
when (content.type) {
|
||||||
|
"image" -> {
|
||||||
|
val export = lifecycleScope.async {
|
||||||
|
Compatibility.addImageToMediaStore(requireContext(), content)
|
||||||
|
}
|
||||||
|
if (export.await()) {
|
||||||
|
Log.i("[File Viewer] Adding image ${content.name} to Media Store terminated: ${content.userData}")
|
||||||
|
mediaStoreFilePath = content.userData.toString()
|
||||||
|
} else {
|
||||||
|
Log.e("[File Viewer] Something went wrong while copying file to Media Store...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"video" -> {
|
||||||
|
val export = lifecycleScope.async {
|
||||||
|
Compatibility.addVideoToMediaStore(requireContext(), content)
|
||||||
|
}
|
||||||
|
if (export.await()) {
|
||||||
|
Log.i("[File Viewer] Adding video ${content.name} to Media Store terminated: ${content.userData}")
|
||||||
|
mediaStoreFilePath = content.userData.toString()
|
||||||
|
} else {
|
||||||
|
Log.e("[File Viewer] Something went wrong while copying file to Media Store...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"audio" -> {
|
||||||
|
val export = lifecycleScope.async {
|
||||||
|
Compatibility.addAudioToMediaStore(requireContext(), content)
|
||||||
|
}
|
||||||
|
if (export.await()) {
|
||||||
|
Log.i("[File Viewer] Adding audio ${content.name} to Media Store terminated: ${content.userData}")
|
||||||
|
mediaStoreFilePath = content.userData.toString()
|
||||||
|
} else {
|
||||||
|
Log.e("[File Viewer] Something went wrong while copying file to Media Store...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.w("[File Viewer] File ${content.name} isn't either an image, an audio file or a video, can't add it to the Media Store")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w("[File Viewer] Can't export image through Media Store API (requires Android 10 or WRITE_EXTERNAL permission, using fallback method...")
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (mediaStoreFilePath.isEmpty()) {
|
||||||
|
Log.w("[File Viewer] Media store file path is empty, media store export failed?")
|
||||||
|
|
||||||
|
val filePath = content.plainFilePath.orEmpty()
|
||||||
|
plainFilePath = filePath.ifEmpty { content.filePath.orEmpty() }
|
||||||
|
Log.i("[File Viewer] Plain file path is: $plainFilePath")
|
||||||
|
if (plainFilePath.isNotEmpty()) {
|
||||||
|
if (!FileUtils.openFileInThirdPartyApp(requireActivity(), plainFilePath)) {
|
||||||
|
(requireActivity() as SnackBarActivity).showSnackBar(R.string.chat_message_no_app_found_to_handle_file_mime_type)
|
||||||
|
if (plainFilePath != content.filePath.orEmpty()) {
|
||||||
|
Log.i("[File Viewer] No app to open plain file path: $plainFilePath, destroying it")
|
||||||
|
FileUtils.deleteFile(plainFilePath)
|
||||||
|
}
|
||||||
|
plainFilePath = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
plainFilePath = ""
|
||||||
|
Log.i("[File Viewer] Media store file path is: $mediaStoreFilePath")
|
||||||
|
FileUtils.openMediaStoreFile(requireActivity(), mediaStoreFilePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,12 @@ open class FileViewerViewModel(val content: Content) : ViewModel() {
|
||||||
private val deleteAfterUse: Boolean = content.isFileEncrypted
|
private val deleteAfterUse: Boolean = content.isFileEncrypted
|
||||||
|
|
||||||
init {
|
init {
|
||||||
filePath = if (deleteAfterUse) content.plainFilePath else content.filePath.orEmpty()
|
filePath = if (deleteAfterUse) {
|
||||||
|
Log.i("[File Viewer] Content is encrypted, requesting plain file path")
|
||||||
|
content.plainFilePath
|
||||||
|
} else {
|
||||||
|
content.filePath.orEmpty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
|
|
@ -56,35 +56,35 @@ abstract class MasterFragment<T : ViewDataBinding, U : SelectionListAdapter<*, *
|
||||||
listSelectionViewModel = ViewModelProvider(this)[ListTopBarViewModel::class.java]
|
listSelectionViewModel = ViewModelProvider(this)[ListTopBarViewModel::class.java]
|
||||||
|
|
||||||
listSelectionViewModel.isEditionEnabled.observe(
|
listSelectionViewModel.isEditionEnabled.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
if (!it) listSelectionViewModel.onUnSelectAll()
|
if (!it) listSelectionViewModel.onUnSelectAll()
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
listSelectionViewModel.selectAllEvent.observe(
|
listSelectionViewModel.selectAllEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
listSelectionViewModel.onSelectAll(getItemCount() - 1)
|
listSelectionViewModel.onSelectAll(getItemCount() - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
listSelectionViewModel.unSelectAllEvent.observe(
|
listSelectionViewModel.unSelectAllEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
listSelectionViewModel.onUnSelectAll()
|
listSelectionViewModel.onUnSelectAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
listSelectionViewModel.deleteSelectionEvent.observe(
|
listSelectionViewModel.deleteSelectionEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
val confirmationDialog = AppUtils.getStringWithPlural(dialogConfirmationMessageBeforeRemoval, listSelectionViewModel.selectedItems.value.orEmpty().size)
|
val confirmationDialog = AppUtils.getStringWithPlural(
|
||||||
|
dialogConfirmationMessageBeforeRemoval,
|
||||||
|
listSelectionViewModel.selectedItems.value.orEmpty().size
|
||||||
|
)
|
||||||
val viewModel = DialogViewModel(confirmationDialog)
|
val viewModel = DialogViewModel(confirmationDialog)
|
||||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
|
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
|
||||||
|
|
||||||
|
@ -105,7 +105,6 @@ abstract class MasterFragment<T : ViewDataBinding, U : SelectionListAdapter<*, *
|
||||||
dialog.show()
|
dialog.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun delete() {
|
private fun delete() {
|
||||||
|
|
|
@ -51,15 +51,14 @@ class StatusFragment : GenericFragment<StatusFragmentBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
sharedViewModel.accountRemoved.observe(
|
sharedViewModel.accountRemoved.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
Log.i("[Status Fragment] An account was removed, update default account state")
|
Log.i("[Status Fragment] An account was removed, update default account state")
|
||||||
val defaultAccount = coreContext.core.defaultAccount
|
val defaultAccount = coreContext.core.defaultAccount
|
||||||
if (defaultAccount != null) {
|
if (defaultAccount != null) {
|
||||||
viewModel.updateDefaultAccountRegistrationStatus(defaultAccount.state)
|
viewModel.updateDefaultAccountRegistrationStatus(defaultAccount.state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
binding.setMenuClickListener {
|
binding.setMenuClickListener {
|
||||||
sharedViewModel.toggleDrawerEvent.value = Event(true)
|
sharedViewModel.toggleDrawerEvent.value = Event(true)
|
||||||
|
|
|
@ -65,7 +65,7 @@ class CallLogsListAdapter(
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
fun bind(callLogGroup: GroupedCallLogData) {
|
fun bind(callLogGroup: GroupedCallLogData) {
|
||||||
with(binding) {
|
with(binding) {
|
||||||
val callLogViewModel = callLogGroup.lastCallLogViewModel
|
val callLogViewModel = callLogGroup.lastCallLogData
|
||||||
viewModel = callLogViewModel
|
viewModel = callLogViewModel
|
||||||
|
|
||||||
lifecycleOwner = viewLifecycleOwner
|
lifecycleOwner = viewLifecycleOwner
|
||||||
|
@ -73,15 +73,14 @@ class CallLogsListAdapter(
|
||||||
// This is for item selection through ListTopBarFragment
|
// This is for item selection through ListTopBarFragment
|
||||||
selectionListViewModel = selectionViewModel
|
selectionListViewModel = selectionViewModel
|
||||||
selectionViewModel.isEditionEnabled.observe(
|
selectionViewModel.isEditionEnabled.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
position = adapterPosition
|
position = bindingAdapterPosition
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
setClickListener {
|
setClickListener {
|
||||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||||
selectionViewModel.onToggleSelect(adapterPosition)
|
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||||
} else {
|
} else {
|
||||||
startCallToEvent.value = Event(callLogGroup)
|
startCallToEvent.value = Event(callLogGroup)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
package org.linphone.activities.main.history.data
|
||||||
|
|
||||||
import org.linphone.activities.main.history.viewmodels.CallLogViewModel
|
|
||||||
import org.linphone.core.CallLog
|
import org.linphone.core.CallLog
|
||||||
|
|
||||||
class GroupedCallLogData(callLog: CallLog) {
|
class GroupedCallLogData(callLog: CallLog) {
|
||||||
var lastCallLog: CallLog = callLog
|
var lastCallLog: CallLog = callLog
|
||||||
val callLogs = arrayListOf(callLog)
|
val callLogs = arrayListOf(callLog)
|
||||||
val lastCallLogViewModel = CallLogViewModel(lastCallLog)
|
val lastCallLogData = CallLogData(lastCallLog)
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
lastCallLogViewModel.destroy()
|
lastCallLogData.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,7 @@ class DetailCallLogFragment : GenericFragment<HistoryDetailFragmentBinding>() {
|
||||||
|
|
||||||
useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false
|
useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false
|
||||||
|
|
||||||
viewModel.relatedCallLogs.value = callLogGroup.callLogs
|
viewModel.addRelatedCallLogs(callLogGroup.callLogs)
|
||||||
|
|
||||||
binding.setBackClickListener {
|
binding.setBackClickListener {
|
||||||
goBack()
|
goBack()
|
||||||
|
@ -99,13 +99,14 @@ class DetailCallLogFragment : GenericFragment<HistoryDetailFragmentBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.startCallEvent.observe(
|
viewModel.startCallEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { callLog ->
|
it.consume { callLog ->
|
||||||
val address = callLog.remoteAddress
|
val address = callLog.remoteAddress
|
||||||
if (coreContext.core.callsNb > 0) {
|
if (coreContext.core.callsNb > 0) {
|
||||||
Log.i("[History] Starting dialer with pre-filled URI ${address.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}")
|
Log.i("[History] Starting dialer with pre-filled URI ${address.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}")
|
||||||
sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.masterCallLogsFragment)
|
sharedViewModel.updateDialerAnimationsBasedOnDestination.value =
|
||||||
|
Event(R.id.masterCallLogsFragment)
|
||||||
|
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
args.putString("URI", address.asStringUriOnly())
|
args.putString("URI", address.asStringUriOnly())
|
||||||
|
@ -121,11 +122,10 @@ class DetailCallLogFragment : GenericFragment<HistoryDetailFragmentBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.chatRoomCreatedEvent.observe(
|
viewModel.chatRoomCreatedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { chatRoom ->
|
it.consume { chatRoom ->
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
args.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly())
|
args.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly())
|
||||||
|
@ -133,16 +133,14 @@ class DetailCallLogFragment : GenericFragment<HistoryDetailFragmentBinding>() {
|
||||||
navigateToChatRoom(args)
|
navigateToChatRoom(args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.onErrorEvent.observe(
|
viewModel.onErrorEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { messageResourceId ->
|
it.consume { messageResourceId ->
|
||||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun goBack() {
|
override fun goBack() {
|
||||||
|
|
|
@ -101,22 +101,22 @@ class MasterCallLogsFragment : MasterFragment<HistoryMasterFragmentBinding, Call
|
||||||
view.doOnPreDraw { sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable }
|
view.doOnPreDraw { sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable }
|
||||||
|
|
||||||
sharedViewModel.closeSlidingPaneEvent.observe(
|
sharedViewModel.closeSlidingPaneEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
if (!binding.slidingPane.closePane()) {
|
if (!binding.slidingPane.closePane()) {
|
||||||
goBack()
|
goBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
sharedViewModel.layoutChangedEvent.observe(
|
sharedViewModel.layoutChangedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
|
sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
|
||||||
if (binding.slidingPane.isSlideable) {
|
if (binding.slidingPane.isSlideable) {
|
||||||
val navHostFragment = childFragmentManager.findFragmentById(R.id.history_nav_container) as NavHostFragment
|
val navHostFragment =
|
||||||
|
childFragmentManager.findFragmentById(R.id.history_nav_container) as NavHostFragment
|
||||||
if (navHostFragment.navController.currentDestination?.id == R.id.emptyCallHistoryFragment) {
|
if (navHostFragment.navController.currentDestination?.id == R.id.emptyCallHistoryFragment) {
|
||||||
Log.i("[History] Foldable device has been folded, closing side pane with empty fragment")
|
Log.i("[History] Foldable device has been folded, closing side pane with empty fragment")
|
||||||
binding.slidingPane.closePane()
|
binding.slidingPane.closePane()
|
||||||
|
@ -124,23 +124,7 @@ class MasterCallLogsFragment : MasterFragment<HistoryMasterFragmentBinding, Call
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
|
binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
|
||||||
/*binding.slidingPane.addPanelSlideListener(object : SlidingPaneLayout.PanelSlideListener {
|
|
||||||
override fun onPanelSlide(panel: View, slideOffset: Float) { }
|
|
||||||
|
|
||||||
override fun onPanelOpened(panel: View) {
|
|
||||||
if (binding.slidingPane.isSlideable) {
|
|
||||||
(requireActivity() as MainActivity).hideTabsFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPanelClosed(panel: View) {
|
|
||||||
if (binding.slidingPane.isSlideable) {
|
|
||||||
(requireActivity() as MainActivity).showTabsFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})*/
|
|
||||||
|
|
||||||
/* End of shared view model & sliding pane related */
|
/* End of shared view model & sliding pane related */
|
||||||
|
|
||||||
|
@ -175,13 +159,13 @@ class MasterCallLogsFragment : MasterFragment<HistoryMasterFragmentBinding, Call
|
||||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
|
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
|
||||||
|
|
||||||
viewModel.showCancelButton {
|
viewModel.showCancelButton {
|
||||||
adapter.notifyItemChanged(viewHolder.adapterPosition)
|
adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.showDeleteButton(
|
viewModel.showDeleteButton(
|
||||||
{
|
{
|
||||||
val deletedCallGroup = adapter.currentList[viewHolder.adapterPosition]
|
val deletedCallGroup = adapter.currentList[viewHolder.bindingAdapterPosition]
|
||||||
listViewModel.deleteCallLogGroup(deletedCallGroup)
|
listViewModel.deleteCallLogGroup(deletedCallGroup)
|
||||||
if (!binding.slidingPane.isSlideable &&
|
if (!binding.slidingPane.isSlideable &&
|
||||||
deletedCallGroup.lastCallLog.callId == sharedViewModel.selectedCallLogGroup.value?.lastCallLog?.callId
|
deletedCallGroup.lastCallLog.callId == sharedViewModel.selectedCallLogGroup.value?.lastCallLog?.callId
|
||||||
|
@ -208,65 +192,62 @@ class MasterCallLogsFragment : MasterFragment<HistoryMasterFragmentBinding, Call
|
||||||
binding.callLogsList.addItemDecoration(headerItemDecoration)
|
binding.callLogsList.addItemDecoration(headerItemDecoration)
|
||||||
|
|
||||||
listViewModel.callLogs.observe(
|
listViewModel.callLogs.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{ callLogs ->
|
) { callLogs ->
|
||||||
if (listViewModel.missedCallLogsSelected.value == false) {
|
if (listViewModel.missedCallLogsSelected.value == false) {
|
||||||
adapter.submitList(callLogs)
|
adapter.submitList(callLogs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
listViewModel.missedCallLogs.observe(
|
listViewModel.missedCallLogs.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{ callLogs ->
|
) { callLogs ->
|
||||||
if (listViewModel.missedCallLogsSelected.value == true) {
|
if (listViewModel.missedCallLogsSelected.value == true) {
|
||||||
adapter.submitList(callLogs)
|
adapter.submitList(callLogs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
listViewModel.missedCallLogsSelected.observe(
|
listViewModel.missedCallLogsSelected.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
if (it) {
|
if (it) {
|
||||||
adapter.submitList(listViewModel.missedCallLogs.value)
|
adapter.submitList(listViewModel.missedCallLogs.value)
|
||||||
} else {
|
} else {
|
||||||
adapter.submitList(listViewModel.callLogs.value)
|
adapter.submitList(listViewModel.callLogs.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
listViewModel.contactsUpdatedEvent.observe(
|
listViewModel.contactsUpdatedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
adapter.selectedCallLogEvent.observe(
|
adapter.selectedCallLogEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { callLog ->
|
it.consume { callLog ->
|
||||||
sharedViewModel.selectedCallLogGroup.value = callLog
|
sharedViewModel.selectedCallLogGroup.value = callLog
|
||||||
navigateToCallHistory(binding.slidingPane)
|
navigateToCallHistory(binding.slidingPane)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
adapter.startCallToEvent.observe(
|
adapter.startCallToEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { callLogGroup ->
|
it.consume { callLogGroup ->
|
||||||
val remoteAddress = callLogGroup.lastCallLog.remoteAddress
|
val remoteAddress = callLogGroup.lastCallLog.remoteAddress
|
||||||
if (coreContext.core.callsNb > 0) {
|
if (coreContext.core.callsNb > 0) {
|
||||||
Log.i("[History] Starting dialer with pre-filled URI ${remoteAddress.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}")
|
Log.i("[History] Starting dialer with pre-filled URI ${remoteAddress.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}")
|
||||||
sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event(R.id.masterCallLogsFragment)
|
sharedViewModel.updateDialerAnimationsBasedOnDestination.value =
|
||||||
|
Event(R.id.masterCallLogsFragment)
|
||||||
val args = Bundle()
|
val args = Bundle()
|
||||||
args.putString("URI", remoteAddress.asStringUriOnly())
|
args.putString("URI", remoteAddress.asStringUriOnly())
|
||||||
args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer)
|
args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer)
|
||||||
args.putBoolean("SkipAutoCallStart", true) // If auto start call setting is enabled, ignore it
|
// If auto start call setting is enabled, ignore it
|
||||||
|
args.putBoolean("SkipAutoCallStart", true)
|
||||||
navigateToDialer(args)
|
navigateToDialer(args)
|
||||||
} else {
|
} else {
|
||||||
val localAddress = callLogGroup.lastCallLog.localAddress
|
val localAddress = callLogGroup.lastCallLog.localAddress
|
||||||
|
@ -274,7 +255,6 @@ class MasterCallLogsFragment : MasterFragment<HistoryMasterFragmentBinding, Call
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
binding.setAllCallLogsToggleClickListener {
|
binding.setAllCallLogsToggleClickListener {
|
||||||
listViewModel.missedCallLogsSelected.value = false
|
listViewModel.missedCallLogsSelected.value = false
|
||||||
|
|
|
@ -22,17 +22,16 @@ package org.linphone.activities.main.history.viewmodels
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||||
import org.linphone.R
|
import org.linphone.R
|
||||||
|
import org.linphone.activities.main.history.data.CallLogData
|
||||||
import org.linphone.contact.GenericContactViewModel
|
import org.linphone.contact.GenericContactViewModel
|
||||||
import org.linphone.core.*
|
import org.linphone.core.*
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.utils.Event
|
import org.linphone.utils.Event
|
||||||
import org.linphone.utils.LinphoneUtils
|
import org.linphone.utils.LinphoneUtils
|
||||||
import org.linphone.utils.TimestampUtils
|
|
||||||
|
|
||||||
class CallLogViewModelFactory(private val callLog: CallLog) :
|
class CallLogViewModelFactory(private val callLog: CallLog) :
|
||||||
ViewModelProvider.NewInstanceFactory() {
|
ViewModelProvider.NewInstanceFactory() {
|
||||||
|
@ -48,53 +47,6 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
|
||||||
LinphoneUtils.getDisplayableAddress(callLog.remoteAddress)
|
LinphoneUtils.getDisplayableAddress(callLog.remoteAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
val statusIconResource: Int by lazy {
|
|
||||||
if (callLog.dir == Call.Dir.Incoming) {
|
|
||||||
if (callLog.status == Call.Status.Missed) {
|
|
||||||
R.drawable.call_status_missed
|
|
||||||
} else {
|
|
||||||
R.drawable.call_status_incoming
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
R.drawable.call_status_outgoing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val iconContentDescription: Int by lazy {
|
|
||||||
if (callLog.dir == Call.Dir.Incoming) {
|
|
||||||
if (callLog.status == Call.Status.Missed) {
|
|
||||||
R.string.content_description_missed_call
|
|
||||||
} else {
|
|
||||||
R.string.content_description_incoming_call
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
R.string.content_description_outgoing_call
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val directionIconResource: Int by lazy {
|
|
||||||
if (callLog.dir == Call.Dir.Incoming) {
|
|
||||||
if (callLog.status == Call.Status.Missed) {
|
|
||||||
R.drawable.call_missed
|
|
||||||
} else {
|
|
||||||
R.drawable.call_incoming
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
R.drawable.call_outgoing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val duration: String by lazy {
|
|
||||||
val dateFormat = SimpleDateFormat(if (callLog.duration >= 3600) "HH:mm:ss" else "mm:ss", Locale.getDefault())
|
|
||||||
val cal = Calendar.getInstance()
|
|
||||||
cal[0, 0, 0, 0, 0] = callLog.duration
|
|
||||||
dateFormat.format(cal.time)
|
|
||||||
}
|
|
||||||
|
|
||||||
val date: String by lazy {
|
|
||||||
TimestampUtils.toString(callLog.startDate, shortDate = false, hideYear = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
val startCallEvent: MutableLiveData<Event<CallLog>> by lazy {
|
val startCallEvent: MutableLiveData<Event<CallLog>> by lazy {
|
||||||
MutableLiveData<Event<CallLog>>()
|
MutableLiveData<Event<CallLog>>()
|
||||||
}
|
}
|
||||||
|
@ -109,7 +61,16 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
|
||||||
|
|
||||||
val secureChatAllowed = contact.value?.friend?.getPresenceModelForUriOrTel(peerSipUri)?.hasCapability(FriendCapability.LimeX3Dh) ?: false
|
val secureChatAllowed = contact.value?.friend?.getPresenceModelForUriOrTel(peerSipUri)?.hasCapability(FriendCapability.LimeX3Dh) ?: false
|
||||||
|
|
||||||
val relatedCallLogs = MutableLiveData<ArrayList<CallLog>>()
|
val relatedCallLogs = MutableLiveData<ArrayList<CallLogData>>()
|
||||||
|
|
||||||
|
private val listener = object : CoreListenerStub() {
|
||||||
|
override fun onCallLogUpdated(core: Core, log: CallLog) {
|
||||||
|
if (callLog.remoteAddress.weakEqual(log.remoteAddress) && callLog.localAddress.weakEqual(log.localAddress)) {
|
||||||
|
Log.i("[History Detail] New call log for ${callLog.remoteAddress.asStringUriOnly()} with local address ${callLog.localAddress.asStringUriOnly()}")
|
||||||
|
addRelatedCallLogs(arrayListOf(log))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val chatRoomListener = object : ChatRoomListenerStub() {
|
private val chatRoomListener = object : ChatRoomListenerStub() {
|
||||||
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
|
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||||
|
@ -126,14 +87,19 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
|
||||||
|
|
||||||
init {
|
init {
|
||||||
waitForChatRoomCreation.value = false
|
waitForChatRoomCreation.value = false
|
||||||
|
|
||||||
|
coreContext.core.addListener(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
coreContext.core.removeListener(listener)
|
||||||
destroy()
|
destroy()
|
||||||
|
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
|
relatedCallLogs.value.orEmpty().forEach(CallLogData::destroy)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startCall() {
|
fun startCall() {
|
||||||
|
@ -157,11 +123,15 @@ class CallLogViewModel(val callLog: CallLog) : GenericContactViewModel(callLog.r
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCallsHistory(): ArrayList<CallLogViewModel> {
|
fun addRelatedCallLogs(logs: ArrayList<CallLog>) {
|
||||||
val callsHistory = ArrayList<CallLogViewModel>()
|
val callsHistory = ArrayList<CallLogData>()
|
||||||
for (callLog in relatedCallLogs.value.orEmpty()) {
|
|
||||||
callsHistory.add(CallLogViewModel(callLog))
|
// We assume new logs are more recent than the ones we already have, so we add them first
|
||||||
|
for (log in logs) {
|
||||||
|
callsHistory.add(CallLogData(log))
|
||||||
}
|
}
|
||||||
return callsHistory
|
callsHistory.addAll(relatedCallLogs.value.orEmpty())
|
||||||
|
|
||||||
|
relatedCallLogs.value = callsHistory
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,12 +69,12 @@ class RecordingsListAdapter(
|
||||||
lifecycleOwner = viewLifecycleOwner
|
lifecycleOwner = viewLifecycleOwner
|
||||||
|
|
||||||
// This is for item selection through ListTopBarFragment
|
// This is for item selection through ListTopBarFragment
|
||||||
position = adapterPosition
|
position = bindingAdapterPosition
|
||||||
selectionListViewModel = selectionViewModel
|
selectionListViewModel = selectionViewModel
|
||||||
|
|
||||||
setClickListener {
|
setClickListener {
|
||||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||||
selectionViewModel.onToggleSelect(adapterPosition)
|
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -164,8 +164,9 @@ class RecordingData(val path: String, private val recordingListener: RecordingLi
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initPlayer() {
|
private fun initPlayer() {
|
||||||
// Use speaker sound card to play recordings, otherwise use earpiece
|
// In case no headphones/headset is connected, use speaker sound card to play recordings, otherwise use earpiece
|
||||||
// If none are available, default one will be used
|
// If none are available, default one will be used
|
||||||
|
var headphonesCard: String? = null
|
||||||
var speakerCard: String? = null
|
var speakerCard: String? = null
|
||||||
var earpieceCard: String? = null
|
var earpieceCard: String? = null
|
||||||
for (device in coreContext.core.audioDevices) {
|
for (device in coreContext.core.audioDevices) {
|
||||||
|
@ -174,11 +175,14 @@ class RecordingData(val path: String, private val recordingListener: RecordingLi
|
||||||
speakerCard = device.id
|
speakerCard = device.id
|
||||||
} else if (device.type == AudioDevice.Type.Earpiece) {
|
} else if (device.type == AudioDevice.Type.Earpiece) {
|
||||||
earpieceCard = device.id
|
earpieceCard = device.id
|
||||||
|
} else if (device.type == AudioDevice.Type.Headphones || device.type == AudioDevice.Type.Headset) {
|
||||||
|
headphonesCard = device.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Log.i("[Recording VM] Found headset/headphones sound card [$headphonesCard], speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]")
|
||||||
|
|
||||||
val localPlayer = coreContext.core.createLocalPlayer(speakerCard ?: earpieceCard, null, null)
|
val localPlayer = coreContext.core.createLocalPlayer(headphonesCard ?: speakerCard ?: earpieceCard, null, null)
|
||||||
if (localPlayer != null) player = localPlayer
|
if (localPlayer != null) player = localPlayer
|
||||||
else Log.e("[Recording VM] Couldn't create local player!")
|
else Log.e("[Recording VM] Couldn't create local player!")
|
||||||
player.addListener(listener)
|
player.addListener(listener)
|
||||||
|
|
|
@ -29,6 +29,7 @@ import org.linphone.activities.main.fragments.MasterFragment
|
||||||
import org.linphone.activities.main.recordings.adapters.RecordingsListAdapter
|
import org.linphone.activities.main.recordings.adapters.RecordingsListAdapter
|
||||||
import org.linphone.activities.main.recordings.data.RecordingData
|
import org.linphone.activities.main.recordings.data.RecordingData
|
||||||
import org.linphone.activities.main.recordings.viewmodels.RecordingsViewModel
|
import org.linphone.activities.main.recordings.viewmodels.RecordingsViewModel
|
||||||
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.databinding.RecordingsFragmentBinding
|
import org.linphone.databinding.RecordingsFragmentBinding
|
||||||
import org.linphone.utils.AppUtils
|
import org.linphone.utils.AppUtils
|
||||||
import org.linphone.utils.RecyclerViewHeaderDecoration
|
import org.linphone.utils.RecyclerViewHeaderDecoration
|
||||||
|
@ -69,11 +70,10 @@ class RecordingsFragment : MasterFragment<RecordingsFragmentBinding, RecordingsL
|
||||||
binding.recordingsList.addItemDecoration(headerItemDecoration)
|
binding.recordingsList.addItemDecoration(headerItemDecoration)
|
||||||
|
|
||||||
viewModel.recordingsList.observe(
|
viewModel.recordingsList.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{ recordings ->
|
) { recordings ->
|
||||||
adapter.submitList(recordings)
|
adapter.submitList(recordings)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
binding.setBackClickListener { goBack() }
|
binding.setBackClickListener { goBack() }
|
||||||
|
|
||||||
|
@ -111,4 +111,13 @@ class RecordingsFragment : MasterFragment<RecordingsFragmentBinding, RecordingsL
|
||||||
}
|
}
|
||||||
viewModel.deleteRecordings(list)
|
viewModel.deleteRecordings(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
if (this::viewModel.isInitialized) {
|
||||||
|
viewModel.udpdateRecordingsList()
|
||||||
|
} else {
|
||||||
|
Log.e("[Recordings] Fragment resuming but viewModel lateinit property isn't initialized!")
|
||||||
|
}
|
||||||
|
super.onResume()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,7 +59,6 @@ class RecordingsViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
getRecordings()
|
|
||||||
isVideoVisible.value = false
|
isVideoVisible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,10 +85,10 @@ class RecordingsViewModel : ViewModel() {
|
||||||
FileUtils.deleteFile(recording.path)
|
FileUtils.deleteFile(recording.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
getRecordings()
|
udpdateRecordingsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getRecordings() {
|
fun udpdateRecordingsList() {
|
||||||
recordingsList.value.orEmpty().forEach(RecordingData::destroy)
|
recordingsList.value.orEmpty().forEach(RecordingData::destroy)
|
||||||
val list = arrayListOf<RecordingData>()
|
val list = arrayListOf<RecordingData>()
|
||||||
|
|
||||||
|
|
|
@ -63,8 +63,8 @@ class AccountSettingsFragment : GenericSettingFragment<SettingsAccountFragmentBi
|
||||||
binding.setBackClickListener { goBack() }
|
binding.setBackClickListener { goBack() }
|
||||||
|
|
||||||
viewModel.linkPhoneNumberEvent.observe(
|
viewModel.linkPhoneNumberEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
val authInfo = viewModel.account.findAuthInfo()
|
val authInfo = viewModel.account.findAuthInfo()
|
||||||
if (authInfo == null) {
|
if (authInfo == null) {
|
||||||
|
@ -78,17 +78,15 @@ class AccountSettingsFragment : GenericSettingFragment<SettingsAccountFragmentBi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.accountRemovedEvent.observe(
|
viewModel.accountRemovedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
sharedViewModel.accountRemoved.value = true
|
sharedViewModel.accountRemoved.value = true
|
||||||
goBack()
|
goBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
view.doOnPreDraw {
|
view.doOnPreDraw {
|
||||||
// Notifies fragment is ready to be drawn
|
// Notifies fragment is ready to be drawn
|
||||||
|
|
|
@ -55,8 +55,8 @@ class AdvancedSettingsFragment : GenericSettingFragment<SettingsAdvancedFragment
|
||||||
binding.setBackClickListener { goBack() }
|
binding.setBackClickListener { goBack() }
|
||||||
|
|
||||||
viewModel.uploadFinishedEvent.observe(
|
viewModel.uploadFinishedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { url ->
|
it.consume { url ->
|
||||||
val clipboard =
|
val clipboard =
|
||||||
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
@ -69,31 +69,28 @@ class AdvancedSettingsFragment : GenericSettingFragment<SettingsAdvancedFragment
|
||||||
AppUtils.shareUploadedLogsUrl(activity, url)
|
AppUtils.shareUploadedLogsUrl(activity, url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.uploadErrorEvent.observe(
|
viewModel.uploadErrorEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
val activity = requireActivity() as MainActivity
|
val activity = requireActivity() as MainActivity
|
||||||
activity.showSnackBar(R.string.logs_upload_failure)
|
activity.showSnackBar(R.string.logs_upload_failure)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.resetCompleteEvent.observe(
|
viewModel.resetCompleteEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
val activity = requireActivity() as MainActivity
|
val activity = requireActivity() as MainActivity
|
||||||
activity.showSnackBar(R.string.logs_reset_complete)
|
activity.showSnackBar(R.string.logs_reset_complete)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.setNightModeEvent.observe(
|
viewModel.setNightModeEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { value ->
|
it.consume { value ->
|
||||||
AppCompatDelegate.setDefaultNightMode(
|
AppCompatDelegate.setDefaultNightMode(
|
||||||
when (value) {
|
when (value) {
|
||||||
|
@ -104,13 +101,12 @@ class AdvancedSettingsFragment : GenericSettingFragment<SettingsAdvancedFragment
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.backgroundModeEnabled.value = !DeviceUtils.isAppUserRestricted(requireContext())
|
viewModel.backgroundModeEnabled.value = !DeviceUtils.isAppUserRestricted(requireContext())
|
||||||
|
|
||||||
viewModel.goToBatterySettingsEvent.observe(
|
viewModel.goToBatterySettingsEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
try {
|
try {
|
||||||
val intent = Intent("android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS")
|
val intent = Intent("android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS")
|
||||||
|
@ -120,12 +116,11 @@ class AdvancedSettingsFragment : GenericSettingFragment<SettingsAdvancedFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.powerManagerSettingsVisibility.value = PowerManagerUtils.getDevicePowerManagerIntent(requireContext()) != null
|
viewModel.powerManagerSettingsVisibility.value = PowerManagerUtils.getDevicePowerManagerIntent(requireContext()) != null
|
||||||
viewModel.goToPowerManagerSettingsEvent.observe(
|
viewModel.goToPowerManagerSettingsEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
val intent = PowerManagerUtils.getDevicePowerManagerIntent(requireActivity())
|
val intent = PowerManagerUtils.getDevicePowerManagerIntent(requireActivity())
|
||||||
if (intent != null) {
|
if (intent != null) {
|
||||||
|
@ -137,11 +132,10 @@ class AdvancedSettingsFragment : GenericSettingFragment<SettingsAdvancedFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.goToAndroidSettingsEvent.observe(
|
viewModel.goToAndroidSettingsEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
||||||
|
@ -151,7 +145,6 @@ class AdvancedSettingsFragment : GenericSettingFragment<SettingsAdvancedFragment
|
||||||
ContextCompat.startActivity(requireContext(), intent, null)
|
ContextCompat.startActivity(requireContext(), intent, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun goBack() {
|
override fun goBack() {
|
||||||
|
|
|
@ -54,24 +54,22 @@ class AudioSettingsFragment : GenericSettingFragment<SettingsAudioFragmentBindin
|
||||||
binding.setBackClickListener { goBack() }
|
binding.setBackClickListener { goBack() }
|
||||||
|
|
||||||
viewModel.askAudioRecordPermissionForEchoCancellerCalibrationEvent.observe(
|
viewModel.askAudioRecordPermissionForEchoCancellerCalibrationEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
Log.i("[Audio Settings] Asking for RECORD_AUDIO permission for echo canceller calibration")
|
Log.i("[Audio Settings] Asking for RECORD_AUDIO permission for echo canceller calibration")
|
||||||
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 1)
|
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.askAudioRecordPermissionForEchoTesterEvent.observe(
|
viewModel.askAudioRecordPermissionForEchoTesterEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
Log.i("[Audio Settings] Asking for RECORD_AUDIO permission for echo tester")
|
Log.i("[Audio Settings] Asking for RECORD_AUDIO permission for echo tester")
|
||||||
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 2)
|
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
initAudioCodecsList()
|
initAudioCodecsList()
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,6 @@
|
||||||
*/
|
*/
|
||||||
package org.linphone.activities.main.settings.fragments
|
package org.linphone.activities.main.settings.fragments
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
@ -38,7 +37,6 @@ import org.linphone.databinding.SettingsCallFragmentBinding
|
||||||
import org.linphone.mediastream.Version
|
import org.linphone.mediastream.Version
|
||||||
import org.linphone.telecom.TelecomHelper
|
import org.linphone.telecom.TelecomHelper
|
||||||
import org.linphone.utils.Event
|
import org.linphone.utils.Event
|
||||||
import org.linphone.utils.PermissionHelper
|
|
||||||
|
|
||||||
class CallSettingsFragment : GenericSettingFragment<SettingsCallFragmentBinding>() {
|
class CallSettingsFragment : GenericSettingFragment<SettingsCallFragmentBinding>() {
|
||||||
private lateinit var viewModel: CallSettingsViewModel
|
private lateinit var viewModel: CallSettingsViewModel
|
||||||
|
@ -57,20 +55,22 @@ class CallSettingsFragment : GenericSettingFragment<SettingsCallFragmentBinding>
|
||||||
binding.setBackClickListener { goBack() }
|
binding.setBackClickListener { goBack() }
|
||||||
|
|
||||||
viewModel.systemWideOverlayEnabledEvent.observe(
|
viewModel.systemWideOverlayEnabledEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
if (!Compatibility.canDrawOverlay(requireContext())) {
|
if (!Compatibility.canDrawOverlay(requireContext())) {
|
||||||
val intent = Intent("android.settings.action.MANAGE_OVERLAY_PERMISSION", Uri.parse("package:${requireContext().packageName}"))
|
val intent = Intent(
|
||||||
|
"android.settings.action.MANAGE_OVERLAY_PERMISSION",
|
||||||
|
Uri.parse("package:${requireContext().packageName}")
|
||||||
|
)
|
||||||
startActivityForResult(intent, 0)
|
startActivityForResult(intent, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.goToAndroidNotificationSettingsEvent.observe(
|
viewModel.goToAndroidNotificationSettingsEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
if (Build.VERSION.SDK_INT >= Version.API26_O_80) {
|
if (Build.VERSION.SDK_INT >= Version.API26_O_80) {
|
||||||
val i = Intent()
|
val i = Intent()
|
||||||
|
@ -88,18 +88,13 @@ class CallSettingsFragment : GenericSettingFragment<SettingsCallFragmentBinding>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.enableTelecomManagerEvent.observe(
|
viewModel.enableTelecomManagerEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
if (!PermissionHelper.get().hasTelecomManagerPermissions()) {
|
if (!Compatibility.hasTelecomManagerPermissions(requireContext())) {
|
||||||
val permissions = arrayOf(
|
Compatibility.requestTelecomManagerPermissions(requireActivity(), 1)
|
||||||
Manifest.permission.READ_PHONE_NUMBERS,
|
|
||||||
Manifest.permission.MANAGE_OWN_CALLS
|
|
||||||
)
|
|
||||||
requestPermissions(permissions, 1)
|
|
||||||
} else if (!TelecomHelper.exists()) {
|
} else if (!TelecomHelper.exists()) {
|
||||||
corePreferences.useTelecomManager = true
|
corePreferences.useTelecomManager = true
|
||||||
Log.w("[Telecom Helper] Doesn't exists yet, creating it")
|
Log.w("[Telecom Helper] Doesn't exists yet, creating it")
|
||||||
|
@ -108,11 +103,10 @@ class CallSettingsFragment : GenericSettingFragment<SettingsCallFragmentBinding>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.goToAndroidNotificationSettingsEvent.observe(
|
viewModel.goToAndroidNotificationSettingsEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
if (Build.VERSION.SDK_INT >= Version.API26_O_80) {
|
if (Build.VERSION.SDK_INT >= Version.API26_O_80) {
|
||||||
val i = Intent()
|
val i = Intent()
|
||||||
|
@ -130,7 +124,6 @@ class CallSettingsFragment : GenericSettingFragment<SettingsCallFragmentBinding>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
|
|
@ -50,8 +50,8 @@ class ChatSettingsFragment : GenericSettingFragment<SettingsChatFragmentBinding>
|
||||||
binding.setBackClickListener { goBack() }
|
binding.setBackClickListener { goBack() }
|
||||||
|
|
||||||
viewModel.launcherShortcutsEvent.observe(
|
viewModel.launcherShortcutsEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { newValue ->
|
it.consume { newValue ->
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
Compatibility.createShortcutsToChatRooms(requireContext())
|
Compatibility.createShortcutsToChatRooms(requireContext())
|
||||||
|
@ -60,11 +60,10 @@ class ChatSettingsFragment : GenericSettingFragment<SettingsChatFragmentBinding>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.goToAndroidNotificationSettingsEvent.observe(
|
viewModel.goToAndroidNotificationSettingsEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
if (Build.VERSION.SDK_INT >= Version.API26_O_80) {
|
if (Build.VERSION.SDK_INT >= Version.API26_O_80) {
|
||||||
val i = Intent()
|
val i = Intent()
|
||||||
|
@ -82,7 +81,6 @@ class ChatSettingsFragment : GenericSettingFragment<SettingsChatFragmentBinding>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun goBack() {
|
override fun goBack() {
|
||||||
|
|
|
@ -51,8 +51,8 @@ class ContactsSettingsFragment : GenericSettingFragment<SettingsContactsFragment
|
||||||
binding.setBackClickListener { goBack() }
|
binding.setBackClickListener { goBack() }
|
||||||
|
|
||||||
viewModel.launcherShortcutsEvent.observe(
|
viewModel.launcherShortcutsEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume { newValue ->
|
it.consume { newValue ->
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
Compatibility.createShortcutsToContacts(requireContext())
|
Compatibility.createShortcutsToContacts(requireContext())
|
||||||
|
@ -64,17 +64,15 @@ class ContactsSettingsFragment : GenericSettingFragment<SettingsContactsFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.askWriteContactsPermissionForPresenceStorageEvent.observe(
|
viewModel.askWriteContactsPermissionForPresenceStorageEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
Log.i("[Contacts Settings] Asking for WRITE_CONTACTS permission to be able to store presence")
|
Log.i("[Contacts Settings] Asking for WRITE_CONTACTS permission to be able to store presence")
|
||||||
requestPermissions(arrayOf(android.Manifest.permission.WRITE_CONTACTS), 1)
|
requestPermissions(arrayOf(android.Manifest.permission.WRITE_CONTACTS), 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
if (!PermissionHelper.required(requireContext()).hasReadContactsPermission()) {
|
if (!PermissionHelper.required(requireContext()).hasReadContactsPermission()) {
|
||||||
Log.i("[Contacts Settings] Asking for READ_CONTACTS permission")
|
Log.i("[Contacts Settings] Asking for READ_CONTACTS permission")
|
||||||
|
|
|
@ -69,31 +69,30 @@ class SettingsFragment : SecureFragment<SettingsFragmentBinding>() {
|
||||||
|
|
||||||
// Account settings loading can take some time, so wait until it is ready before opening the pane
|
// Account settings loading can take some time, so wait until it is ready before opening the pane
|
||||||
sharedViewModel.accountSettingsFragmentOpenedEvent.observe(
|
sharedViewModel.accountSettingsFragmentOpenedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
binding.slidingPane.openPane()
|
binding.slidingPane.openPane()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
sharedViewModel.closeSlidingPaneEvent.observe(
|
sharedViewModel.closeSlidingPaneEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
if (!binding.slidingPane.closePane()) {
|
if (!binding.slidingPane.closePane()) {
|
||||||
goBack()
|
goBack()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
sharedViewModel.layoutChangedEvent.observe(
|
sharedViewModel.layoutChangedEvent.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
it.consume {
|
it.consume {
|
||||||
sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
|
sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
|
||||||
if (binding.slidingPane.isSlideable) {
|
if (binding.slidingPane.isSlideable) {
|
||||||
val navHostFragment = childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment
|
val navHostFragment =
|
||||||
|
childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment
|
||||||
if (navHostFragment.navController.currentDestination?.id == R.id.emptySettingsFragment) {
|
if (navHostFragment.navController.currentDestination?.id == R.id.emptySettingsFragment) {
|
||||||
Log.i("[Settings] Foldable device has been folded, closing side pane with empty fragment")
|
Log.i("[Settings] Foldable device has been folded, closing side pane with empty fragment")
|
||||||
binding.slidingPane.closePane()
|
binding.slidingPane.closePane()
|
||||||
|
@ -101,7 +100,6 @@ class SettingsFragment : SecureFragment<SettingsFragmentBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
|
||||||
binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
|
binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED
|
||||||
|
|
||||||
/* End of shared view model & sliding pane related */
|
/* End of shared view model & sliding pane related */
|
||||||
|
@ -112,12 +110,11 @@ class SettingsFragment : SecureFragment<SettingsFragmentBinding>() {
|
||||||
binding.setBackClickListener { goBack() }
|
binding.setBackClickListener { goBack() }
|
||||||
|
|
||||||
sharedViewModel.accountRemoved.observe(
|
sharedViewModel.accountRemoved.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner
|
||||||
{
|
) {
|
||||||
Log.i("[Settings] Account removed, update accounts list")
|
Log.i("[Settings] Account removed, update accounts list")
|
||||||
viewModel.updateAccountsList()
|
viewModel.updateAccountsList()
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
val identity = arguments?.getString("Identity")
|
val identity = arguments?.getString("Identity")
|
||||||
if (identity != null) {
|
if (identity != null) {
|
||||||
|
|
|
@ -192,7 +192,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
|
||||||
val disableListener = object : SettingListenerStub() {
|
val disableListener = object : SettingListenerStub() {
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
val params = account.params.clone()
|
val params = account.params.clone()
|
||||||
params.registerEnabled = !newValue
|
params.isRegisterEnabled = !newValue
|
||||||
account.params = params
|
account.params = params
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -239,7 +239,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
val params = account.params.clone()
|
val params = account.params.clone()
|
||||||
params.registerEnabled = false
|
params.isRegisterEnabled = false
|
||||||
account.params = params
|
account.params = params
|
||||||
|
|
||||||
if (!registered) {
|
if (!registered) {
|
||||||
|
@ -288,7 +288,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
|
||||||
val outboundProxyListener = object : SettingListenerStub() {
|
val outboundProxyListener = object : SettingListenerStub() {
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
val params = account.params.clone()
|
val params = account.params.clone()
|
||||||
params.outboundProxyEnabled = newValue
|
params.isOutboundProxyEnabled = newValue
|
||||||
account.params = params
|
account.params = params
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -297,8 +297,15 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
|
||||||
val stunServerListener = object : SettingListenerStub() {
|
val stunServerListener = object : SettingListenerStub() {
|
||||||
override fun onTextValueChanged(newValue: String) {
|
override fun onTextValueChanged(newValue: String) {
|
||||||
val params = account.params.clone()
|
val params = account.params.clone()
|
||||||
|
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
|
params.natPolicy?.stunServer = newValue
|
||||||
if (newValue.isEmpty()) ice.value = false
|
if (newValue.isEmpty()) ice.value = false
|
||||||
|
}
|
||||||
stunServer.value = newValue
|
stunServer.value = newValue
|
||||||
account.params = params
|
account.params = params
|
||||||
}
|
}
|
||||||
|
@ -308,7 +315,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
|
||||||
val iceListener = object : SettingListenerStub() {
|
val iceListener = object : SettingListenerStub() {
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
val params = account.params.clone()
|
val params = account.params.clone()
|
||||||
params.natPolicy?.enableIce(newValue)
|
params.natPolicy?.isIceEnabled = newValue
|
||||||
account.params = params
|
account.params = params
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -370,7 +377,7 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
|
||||||
val escapePlusListener = object : SettingListenerStub() {
|
val escapePlusListener = object : SettingListenerStub() {
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
val params = account.params.clone()
|
val params = account.params.clone()
|
||||||
params.dialEscapePlusEnabled = newValue
|
params.isDialEscapePlusEnabled = newValue
|
||||||
account.params = params
|
account.params = params
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -424,19 +431,19 @@ class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel(
|
||||||
userName.value = params.identityAddress?.username
|
userName.value = params.identityAddress?.username
|
||||||
userId.value = account.findAuthInfo()?.userid
|
userId.value = account.findAuthInfo()?.userid
|
||||||
domain.value = params.identityAddress?.domain
|
domain.value = params.identityAddress?.domain
|
||||||
disable.value = !params.registerEnabled
|
disable.value = !params.isRegisterEnabled
|
||||||
pushNotification.value = params.pushNotificationAllowed
|
pushNotification.value = params.pushNotificationAllowed
|
||||||
pushNotificationsAvailable.value = core.isPushNotificationAvailable
|
pushNotificationsAvailable.value = core.isPushNotificationAvailable
|
||||||
proxy.value = params.serverAddress?.asStringUriOnly()
|
proxy.value = params.serverAddress?.asStringUriOnly()
|
||||||
outboundProxy.value = params.outboundProxyEnabled
|
outboundProxy.value = params.isOutboundProxyEnabled
|
||||||
stunServer.value = params.natPolicy?.stunServer
|
stunServer.value = params.natPolicy?.stunServer
|
||||||
ice.value = params.natPolicy?.iceEnabled()
|
ice.value = params.natPolicy?.isIceEnabled
|
||||||
avpf.value = params.avpfMode == AVPFMode.Enabled
|
avpf.value = params.avpfMode == AVPFMode.Enabled
|
||||||
avpfRrInterval.value = params.avpfRrInterval
|
avpfRrInterval.value = params.avpfRrInterval
|
||||||
expires.value = params.expires
|
expires.value = params.expires
|
||||||
prefix.value = params.internationalPrefix
|
prefix.value = params.internationalPrefix
|
||||||
dialPrefix.value = params.useInternationalPrefixForCallsAndChats
|
dialPrefix.value = params.useInternationalPrefixForCallsAndChats
|
||||||
escapePlus.value = params.dialEscapePlusEnabled
|
escapePlus.value = params.isDialEscapePlusEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initTransportList() {
|
private fun initTransportList() {
|
||||||
|
|
|
@ -32,8 +32,8 @@ import org.linphone.mediastream.Version
|
||||||
import org.linphone.utils.Event
|
import org.linphone.utils.Event
|
||||||
|
|
||||||
class AdvancedSettingsViewModel : LogsUploadViewModel() {
|
class AdvancedSettingsViewModel : LogsUploadViewModel() {
|
||||||
protected val prefs = corePreferences
|
private val prefs = corePreferences
|
||||||
protected val core = coreContext.core
|
private val core = coreContext.core
|
||||||
|
|
||||||
val debugModeListener = object : SettingListenerStub() {
|
val debugModeListener = object : SettingListenerStub() {
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
|
|
|
@ -41,7 +41,7 @@ class AudioSettingsViewModel : GenericSettingsViewModel() {
|
||||||
|
|
||||||
val echoCancellationListener = object : SettingListenerStub() {
|
val echoCancellationListener = object : SettingListenerStub() {
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
core.enableEchoCancellation(newValue)
|
core.isEchoCancellationEnabled = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val echoCancellation = MutableLiveData<Boolean>()
|
val echoCancellation = MutableLiveData<Boolean>()
|
||||||
|
@ -81,7 +81,7 @@ class AudioSettingsViewModel : GenericSettingsViewModel() {
|
||||||
|
|
||||||
val adaptiveRateControlListener = object : SettingListenerStub() {
|
val adaptiveRateControlListener = object : SettingListenerStub() {
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
core.enableAdaptiveRateControl(newValue)
|
core.isAdaptiveRateControlEnabled = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val adaptiveRateControl = MutableLiveData<Boolean>()
|
val adaptiveRateControl = MutableLiveData<Boolean>()
|
||||||
|
@ -110,6 +110,13 @@ class AudioSettingsViewModel : GenericSettingsViewModel() {
|
||||||
val outputAudioDeviceLabels = MutableLiveData<ArrayList<String>>()
|
val outputAudioDeviceLabels = MutableLiveData<ArrayList<String>>()
|
||||||
private val outputAudioDeviceValues = MutableLiveData<ArrayList<AudioDevice>>()
|
private val outputAudioDeviceValues = MutableLiveData<ArrayList<AudioDevice>>()
|
||||||
|
|
||||||
|
val preferBluetoothDevicesListener = object : SettingListenerStub() {
|
||||||
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
|
prefs.routeAudioToBluetoothIfAvailable = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val preferBluetoothDevices = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
val codecBitrateListener = object : SettingListenerStub() {
|
val codecBitrateListener = object : SettingListenerStub() {
|
||||||
override fun onListValueChanged(position: Int) {
|
override fun onListValueChanged(position: Int) {
|
||||||
for (payloadType in core.audioPayloadTypes) {
|
for (payloadType in core.audioPayloadTypes) {
|
||||||
|
@ -146,14 +153,15 @@ class AudioSettingsViewModel : GenericSettingsViewModel() {
|
||||||
val audioCodecs = MutableLiveData<ArrayList<ViewDataBinding>>()
|
val audioCodecs = MutableLiveData<ArrayList<ViewDataBinding>>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
echoCancellation.value = core.echoCancellationEnabled()
|
echoCancellation.value = core.isEchoCancellationEnabled
|
||||||
adaptiveRateControl.value = core.adaptiveRateControlEnabled()
|
adaptiveRateControl.value = core.isAdaptiveRateControlEnabled
|
||||||
echoCalibration.value = if (core.echoCancellationEnabled()) {
|
echoCalibration.value = if (core.isEchoCancellationEnabled) {
|
||||||
prefs.getString(R.string.audio_settings_echo_cancellation_calibration_value).format(prefs.echoCancellerCalibration)
|
prefs.getString(R.string.audio_settings_echo_cancellation_calibration_value).format(prefs.echoCancellerCalibration)
|
||||||
} else {
|
} else {
|
||||||
prefs.getString(R.string.audio_settings_echo_canceller_calibration_summary)
|
prefs.getString(R.string.audio_settings_echo_canceller_calibration_summary)
|
||||||
}
|
}
|
||||||
echoTesterStatus.value = prefs.getString(R.string.audio_settings_echo_tester_summary)
|
echoTesterStatus.value = prefs.getString(R.string.audio_settings_echo_tester_summary)
|
||||||
|
preferBluetoothDevices.value = prefs.routeAudioToBluetoothIfAvailable
|
||||||
initInputAudioDevicesList()
|
initInputAudioDevicesList()
|
||||||
initOutputAudioDevicesList()
|
initOutputAudioDevicesList()
|
||||||
initCodecBitrateList()
|
initCodecBitrateList()
|
||||||
|
|
|
@ -20,22 +20,40 @@
|
||||||
package org.linphone.activities.main.settings.viewmodels
|
package org.linphone.activities.main.settings.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import java.io.File
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
import org.linphone.R
|
import org.linphone.R
|
||||||
import org.linphone.activities.main.settings.SettingListenerStub
|
import org.linphone.activities.main.settings.SettingListenerStub
|
||||||
import org.linphone.core.MediaEncryption
|
import org.linphone.core.MediaEncryption
|
||||||
import org.linphone.core.tools.Log
|
import org.linphone.core.tools.Log
|
||||||
import org.linphone.mediastream.Version
|
import org.linphone.mediastream.Version
|
||||||
import org.linphone.telecom.TelecomHelper
|
import org.linphone.telecom.TelecomHelper
|
||||||
|
import org.linphone.utils.AppUtils
|
||||||
import org.linphone.utils.Event
|
import org.linphone.utils.Event
|
||||||
|
|
||||||
class CallSettingsViewModel : GenericSettingsViewModel() {
|
class CallSettingsViewModel : GenericSettingsViewModel() {
|
||||||
val deviceRingtoneListener = object : SettingListenerStub() {
|
val deviceRingtoneListener = object : SettingListenerStub() {
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
core.ring = if (newValue) null else prefs.ringtonePath
|
core.ring = if (newValue) null else prefs.defaultRingtonePath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val deviceRingtone = MutableLiveData<Boolean>()
|
val deviceRingtone = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val ringtoneListener = object : SettingListenerStub() {
|
||||||
|
override fun onListValueChanged(position: Int) {
|
||||||
|
if (position == 0) {
|
||||||
|
core.ring = null
|
||||||
|
} else {
|
||||||
|
core.ring = ringtoneValues[position]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val ringtoneIndex = MutableLiveData<Int>()
|
||||||
|
val ringtoneLabels = MutableLiveData<ArrayList<String>>()
|
||||||
|
private val ringtoneValues = arrayListOf<String>()
|
||||||
|
val showRingtonesList = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
val vibrateOnIncomingCallListener = object : SettingListenerStub() {
|
val vibrateOnIncomingCallListener = object : SettingListenerStub() {
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
core.isVibrationOnIncomingCallEnabled = newValue
|
core.isVibrationOnIncomingCallEnabled = newValue
|
||||||
|
@ -73,6 +91,9 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
|
||||||
TelecomHelper.get().removeAccount()
|
TelecomHelper.get().removeAccount()
|
||||||
TelecomHelper.get().destroy()
|
TelecomHelper.get().destroy()
|
||||||
TelecomHelper.destroy()
|
TelecomHelper.destroy()
|
||||||
|
|
||||||
|
Log.w("[Call Settings] Disabling Telecom Manager auto-enable")
|
||||||
|
prefs.manuallyDisabledTelecomManager = true
|
||||||
}
|
}
|
||||||
prefs.useTelecomManager = newValue
|
prefs.useTelecomManager = newValue
|
||||||
}
|
}
|
||||||
|
@ -209,7 +230,10 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
|
||||||
val goToAndroidNotificationSettingsEvent = MutableLiveData<Event<Boolean>>()
|
val goToAndroidNotificationSettingsEvent = MutableLiveData<Event<Boolean>>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
initRingtonesList()
|
||||||
deviceRingtone.value = core.ring == null
|
deviceRingtone.value = core.ring == null
|
||||||
|
showRingtonesList.value = prefs.showAllRingtones
|
||||||
|
|
||||||
vibrateOnIncomingCall.value = core.isVibrationOnIncomingCallEnabled
|
vibrateOnIncomingCall.value = core.isVibrationOnIncomingCallEnabled
|
||||||
|
|
||||||
initEncryptionList()
|
initEncryptionList()
|
||||||
|
@ -235,6 +259,28 @@ class CallSettingsViewModel : GenericSettingsViewModel() {
|
||||||
pauseCallsWhenAudioFocusIsLost.value = prefs.pauseCallsWhenAudioFocusIsLost
|
pauseCallsWhenAudioFocusIsLost.value = prefs.pauseCallsWhenAudioFocusIsLost
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initRingtonesList() {
|
||||||
|
val labels = arrayListOf<String>()
|
||||||
|
labels.add(AppUtils.getString(R.string.call_settings_device_ringtone_title))
|
||||||
|
ringtoneValues.add("")
|
||||||
|
|
||||||
|
val directory = File(prefs.ringtonesPath)
|
||||||
|
val files = directory.listFiles()
|
||||||
|
for (ringtone in files.orEmpty()) {
|
||||||
|
if (ringtone.absolutePath.endsWith(".mkv")) {
|
||||||
|
val name = ringtone.name
|
||||||
|
.substringBefore(".")
|
||||||
|
.replace("_", " ")
|
||||||
|
.capitalize(Locale.getDefault())
|
||||||
|
labels.add(name)
|
||||||
|
ringtoneValues.add(ringtone.absolutePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ringtoneLabels.value = labels
|
||||||
|
ringtoneIndex.value = if (core.ring == null) 0 else ringtoneValues.indexOf(core.ring)
|
||||||
|
}
|
||||||
|
|
||||||
private fun initEncryptionList() {
|
private fun initEncryptionList() {
|
||||||
val labels = arrayListOf<String>()
|
val labels = arrayListOf<String>()
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ class ChatSettingsViewModel : GenericSettingsViewModel() {
|
||||||
|
|
||||||
val fileSharingUrlListener = object : SettingListenerStub() {
|
val fileSharingUrlListener = object : SettingListenerStub() {
|
||||||
override fun onTextValueChanged(newValue: String) {
|
override fun onTextValueChanged(newValue: String) {
|
||||||
core.logCollectionUploadServerUrl = newValue
|
core.fileTransferServer = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val fileSharingUrl = MutableLiveData<String>()
|
val fileSharingUrl = MutableLiveData<String>()
|
||||||
|
|
|
@ -33,7 +33,7 @@ class ContactsSettingsViewModel : GenericSettingsViewModel() {
|
||||||
|
|
||||||
val friendListSubscribeListener = object : SettingListenerStub() {
|
val friendListSubscribeListener = object : SettingListenerStub() {
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
core.enableFriendListSubscription(newValue)
|
core.isFriendListSubscriptionEnabled = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val friendListSubscribe = MutableLiveData<Boolean>()
|
val friendListSubscribe = MutableLiveData<Boolean>()
|
||||||
|
|
|
@ -26,14 +26,14 @@ import org.linphone.activities.main.settings.SettingListenerStub
|
||||||
class NetworkSettingsViewModel : GenericSettingsViewModel() {
|
class NetworkSettingsViewModel : GenericSettingsViewModel() {
|
||||||
val wifiOnlyListener = object : SettingListenerStub() {
|
val wifiOnlyListener = object : SettingListenerStub() {
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
core.enableWifiOnly(newValue)
|
core.isWifiOnlyEnabled = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val wifiOnly = MutableLiveData<Boolean>()
|
val wifiOnly = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
val allowIpv6Listener = object : SettingListenerStub() {
|
val allowIpv6Listener = object : SettingListenerStub() {
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
core.enableIpv6(newValue)
|
core.isIpv6Enabled = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val allowIpv6 = MutableLiveData<Boolean>()
|
val allowIpv6 = MutableLiveData<Boolean>()
|
||||||
|
@ -59,8 +59,8 @@ class NetworkSettingsViewModel : GenericSettingsViewModel() {
|
||||||
val sipPort = MutableLiveData<Int>()
|
val sipPort = MutableLiveData<Int>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
wifiOnly.value = core.wifiOnlyEnabled()
|
wifiOnly.value = core.isWifiOnlyEnabled
|
||||||
allowIpv6.value = core.ipv6Enabled()
|
allowIpv6.value = core.isIpv6Enabled
|
||||||
randomPorts.value = getTransportPort() == -1
|
randomPorts.value = getTransportPort() == -1
|
||||||
sipPort.value = getTransportPort()
|
sipPort.value = getTransportPort()
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,7 +52,7 @@ class TunnelSettingsViewModel : GenericSettingsViewModel() {
|
||||||
val useDualModeListener = object : SettingListenerStub() {
|
val useDualModeListener = object : SettingListenerStub() {
|
||||||
override fun onBoolValueChanged(newValue: Boolean) {
|
override fun onBoolValueChanged(newValue: Boolean) {
|
||||||
val tunnel = core.tunnel
|
val tunnel = core.tunnel
|
||||||
tunnel?.enableDualMode(newValue)
|
tunnel?.isDualModeEnabled = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val useDualMode = MutableLiveData<Boolean>()
|
val useDualMode = MutableLiveData<Boolean>()
|
||||||
|
@ -96,7 +96,7 @@ class TunnelSettingsViewModel : GenericSettingsViewModel() {
|
||||||
|
|
||||||
hostnameUrl.value = config.host
|
hostnameUrl.value = config.host
|
||||||
port.value = config.port
|
port.value = config.port
|
||||||
useDualMode.value = tunnel?.dualModeEnabled()
|
useDualMode.value = tunnel?.isDualModeEnabled
|
||||||
hostnameUrl2.value = config.host2
|
hostnameUrl2.value = config.host2
|
||||||
port2.value = config.port2
|
port2.value = config.port2
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue