mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 03:18:06 +00:00
6.0.0 cleanup
This commit is contained in:
parent
c627849382
commit
418f9ba4c9
1031 changed files with 0 additions and 87523 deletions
29
.gitignore
vendored
29
.gitignore
vendored
|
|
@ -1,29 +0,0 @@
|
|||
*.orig
|
||||
*.rej
|
||||
.DS_Store
|
||||
.gradle
|
||||
.idea
|
||||
.settings
|
||||
adb.pid
|
||||
bc-android.keystore
|
||||
build
|
||||
*.iml
|
||||
lint.xml
|
||||
local.properties
|
||||
res/.DS_Store
|
||||
res/raw/lpconfig.xsd
|
||||
.d
|
||||
.*clang*
|
||||
**/*.iml
|
||||
**/.classpath
|
||||
**/.project
|
||||
**/*.kdev4
|
||||
**/.vscode
|
||||
res/value-hi_IN
|
||||
linphone-sdk-android/*.aar
|
||||
app/debug
|
||||
app/release
|
||||
app/releaseAppBundle
|
||||
app/releaseWithCrashlytics
|
||||
keystore.properties
|
||||
app/src/main/res/xml/contacts.xml
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
job-android:
|
||||
|
||||
stage: build
|
||||
tags: [ "docker-android" ]
|
||||
image: gitlab.linphone.org:4567/bc/public/linphone-android/bc-dev-android:20230414_bullseye_jdk_17_cleaned
|
||||
|
||||
before_script:
|
||||
- if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then eval $(ssh-agent -s); fi
|
||||
- if ! [ -z ${SCP_PRIVATE_KEY+x} ]; then echo "$SCP_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null; fi
|
||||
- if ! [ -z ${ANDROID_SETTINGS_GRADLE+x} ]; then echo "$ANDROID_SETTINGS_GRADLE" > settings.gradle; fi
|
||||
- git config --global --add safe.directory /builds/BC/public/linphone-android
|
||||
|
||||
script:
|
||||
- scp -oStrictHostKeyChecking=no $DEPLOY_SERVER:$ANDROID_KEYSTORE_PATH app/
|
||||
- scp -oStrictHostKeyChecking=no $DEPLOY_SERVER:$ANDROID_GOOGLE_SERVICES_PATH app/
|
||||
- echo storePassword=$ANDROID_KEYSTORE_PASSWORD > keystore.properties
|
||||
- echo keyPassword=$ANDROID_KEYSTORE_KEY_PASSWORD >> keystore.properties
|
||||
- echo keyAlias=$ANDROID_KEYSTORE_KEY_ALIAS >> keystore.properties
|
||||
- echo storeFile=$ANDROID_KEYSTORE_FILE >> keystore.properties
|
||||
- ./gradlew app:dependencies | grep org.linphone
|
||||
- ./gradlew assembleDebug
|
||||
- ./gradlew assembleRelease
|
||||
|
||||
artifacts:
|
||||
paths:
|
||||
- ./app/build/outputs/apk/debug/linphone-android-debug-*.apk
|
||||
- ./app/build/outputs/apk/release/linphone-android-release-*.apk
|
||||
when: always
|
||||
expire_in: 1 week
|
||||
|
||||
|
||||
.scheduled-job-android:
|
||||
extends: job-android
|
||||
only:
|
||||
- schedules
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
job-android-upload:
|
||||
|
||||
stage: deploy
|
||||
tags: [ "deploy" ]
|
||||
|
||||
only:
|
||||
- schedules
|
||||
dependencies:
|
||||
- job-android
|
||||
|
||||
script:
|
||||
- cd app/build/outputs/apk/ && rsync ./debug/*.apk $DEPLOY_SERVER:$ANDROID_DEPLOY_DIRECTORY
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
#################################################
|
||||
# Base configuration
|
||||
#################################################
|
||||
|
||||
|
||||
|
||||
#################################################
|
||||
# Platforms to test
|
||||
#################################################
|
||||
|
||||
|
||||
include:
|
||||
- '.gitlab-ci-files/job-android.yml'
|
||||
- '.gitlab-ci-files/job-upload.yml'
|
||||
|
||||
|
||||
stages:
|
||||
- build
|
||||
- deploy
|
||||
1
app/.gitignore
vendored
1
app/.gitignore
vendored
|
|
@ -1 +0,0 @@
|
|||
/build
|
||||
285
app/build.gradle
285
app/build.gradle
|
|
@ -1,285 +0,0 @@
|
|||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-kapt'
|
||||
id 'org.jlleitschuh.gradle.ktlint' version '11.3.1'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
|
||||
def appVersionName = "5.3.0"
|
||||
def appVersionCode = 52000
|
||||
|
||||
def packageName = "org.linphone"
|
||||
|
||||
def firebaseAvailable = new File(projectDir.absolutePath +'/google-services.json').exists()
|
||||
|
||||
def crashlyticsAvailable = new File(projectDir.absolutePath +'/google-services.json').exists() && new File(LinphoneSdkBuildDir + '/libs/').exists() && new File(LinphoneSdkBuildDir + '/libs-debug/').exists()
|
||||
|
||||
def extractNativeLibs = false
|
||||
|
||||
if (firebaseAvailable) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
}
|
||||
|
||||
def gitBranch = new ByteArrayOutputStream()
|
||||
task getGitVersion() {
|
||||
def gitVersion = appVersionName
|
||||
def gitVersionStream = new ByteArrayOutputStream()
|
||||
def gitCommitsCount = new ByteArrayOutputStream()
|
||||
def gitCommitHash = new ByteArrayOutputStream()
|
||||
|
||||
try {
|
||||
exec {
|
||||
executable "git" args "describe", "--abbrev=0"
|
||||
standardOutput = gitVersionStream
|
||||
}
|
||||
exec {
|
||||
executable "git" args "rev-list", gitVersionStream.toString().trim() + "..HEAD", "--count"
|
||||
standardOutput = gitCommitsCount
|
||||
}
|
||||
exec {
|
||||
executable "git" args "rev-parse", "--short", "HEAD"
|
||||
standardOutput = gitCommitHash
|
||||
}
|
||||
exec {
|
||||
executable "git" args "name-rev", "--name-only", "HEAD"
|
||||
standardOutput = gitBranch
|
||||
}
|
||||
|
||||
if (gitCommitsCount.toString().toInteger() == 0) {
|
||||
gitVersion = gitVersionStream.toString().trim()
|
||||
} else {
|
||||
gitVersion = gitVersionStream.toString().trim() + "." + gitCommitsCount.toString().trim() + "+" + gitCommitHash.toString().trim()
|
||||
}
|
||||
println("Git version: " + gitVersion + " (" + appVersionCode + ")")
|
||||
} catch (ignored) {
|
||||
println("Git not found, using " + gitVersion + " (" + appVersionCode + ")")
|
||||
}
|
||||
project.version = gitVersion
|
||||
}
|
||||
|
||||
configurations {
|
||||
customImplementation.extendsFrom implementation
|
||||
}
|
||||
|
||||
task linphoneSdkSource() {
|
||||
doLast {
|
||||
configurations.customImplementation.getIncoming().each {
|
||||
it.getResolutionResult().allComponents.each {
|
||||
if (it.id.getDisplayName().contains("linphone-sdk-android")) {
|
||||
println 'Linphone SDK used is ' + it.moduleVersion.version + ' from ' + it.properties["repositoryName"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
project.tasks['preBuild'].dependsOn 'getGitVersion'
|
||||
project.tasks['preBuild'].dependsOn 'linphoneSdkSource'
|
||||
|
||||
android {
|
||||
compileOptions {
|
||||
sourceCompatibility = 17
|
||||
targetCompatibility = 17
|
||||
}
|
||||
|
||||
compileSdkVersion 34
|
||||
defaultConfig {
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 34
|
||||
versionCode appVersionCode
|
||||
versionName "${project.version}"
|
||||
applicationId packageName
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.all {
|
||||
outputFileName = "linphone-android-${variant.buildType.name}-${project.version}.apk"
|
||||
}
|
||||
|
||||
var enableFirebaseService = "false"
|
||||
if (firebaseAvailable) {
|
||||
enableFirebaseService = "true"
|
||||
}
|
||||
|
||||
// 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 == "releaseWithCrashlytics") {
|
||||
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + packageName + ".provider.sip_address",
|
||||
linphone_file_provider: packageName + ".fileprovider",
|
||||
appLabel: "@string/app_name",
|
||||
firebaseServiceEnabled: enableFirebaseService]
|
||||
} else {
|
||||
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + packageName + ".provider.sip_address",
|
||||
linphone_file_provider: packageName + ".debug.fileprovider",
|
||||
appLabel: "@string/app_name_debug",
|
||||
firebaseServiceEnabled: enableFirebaseService]
|
||||
extractNativeLibs = true
|
||||
}
|
||||
}
|
||||
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
def keystoreProperties = new Properties()
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
storeFile file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
signingConfig signingConfigs.release
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
|
||||
resValue "string", "linphone_app_branch", gitBranch.toString().trim()
|
||||
resValue "string", "sync_account_type", packageName + ".sync"
|
||||
resValue "string", "file_provider", packageName + ".fileprovider"
|
||||
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + packageName + ".provider.sip_address"
|
||||
|
||||
if (!firebaseAvailable) {
|
||||
resValue "string", "gcm_defaultSenderId", "none"
|
||||
}
|
||||
|
||||
resValue "bool", "crashlytics_enabled", "false"
|
||||
}
|
||||
|
||||
releaseWithCrashlytics {
|
||||
initWith release
|
||||
|
||||
resValue "bool", "crashlytics_enabled", crashlyticsAvailable.toString()
|
||||
|
||||
if (crashlyticsAvailable) {
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
|
||||
firebaseCrashlytics {
|
||||
nativeSymbolUploadEnabled true
|
||||
unstrippedNativeLibsDir file(LinphoneSdkBuildDir + '/libs-debug/').toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
debuggable true
|
||||
jniDebuggable true
|
||||
|
||||
resValue "string", "linphone_app_branch", gitBranch.toString().trim()
|
||||
resValue "string", "sync_account_type", packageName + ".sync"
|
||||
resValue "string", "file_provider", packageName + ".debug.fileprovider"
|
||||
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + packageName + ".provider.sip_address"
|
||||
resValue "bool", "crashlytics_enabled", crashlyticsAvailable.toString()
|
||||
|
||||
if (!firebaseAvailable) {
|
||||
resValue "string", "gcm_defaultSenderId", "none"
|
||||
}
|
||||
|
||||
if (crashlyticsAvailable) {
|
||||
apply plugin: 'com.google.firebase.crashlytics'
|
||||
|
||||
firebaseCrashlytics {
|
||||
nativeSymbolUploadEnabled false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
dataBinding = true
|
||||
}
|
||||
|
||||
namespace 'org.linphone'
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
useLegacyPackaging extractNativeLibs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
|
||||
implementation 'androidx.media:media:1.6.0'
|
||||
implementation "androidx.security:security-crypto-ktx:1.1.0-alpha06"
|
||||
implementation "androidx.window:window:1.2.0"
|
||||
|
||||
def emoji_version = "1.4.0"
|
||||
implementation "androidx.emoji2:emoji2:$emoji_version"
|
||||
implementation "androidx.emoji2:emoji2-emojipicker:$emoji_version"
|
||||
|
||||
def nav_version = "2.7.5"
|
||||
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
implementation "androidx.slidingpanelayout:slidingpanelayout:1.2.0"
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||
implementation "androidx.gridlayout:gridlayout:1.0.0"
|
||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
||||
implementation 'androidx.drawerlayout:drawerlayout:1.2.0'
|
||||
|
||||
// https://github.com/material-components/material-components-android/blob/master/LICENSE Apache v2.0
|
||||
implementation 'com.google.android.material:material:1.10.0'
|
||||
// https://github.com/google/flexbox-layout/blob/main/LICENSE Apache v2.0
|
||||
implementation 'com.google.android.flexbox:flexbox:3.0.0'
|
||||
|
||||
// https://github.com/coil-kt/coil/blob/main/LICENSE.txt Apache v2.0
|
||||
def coil_version = "2.4.0"
|
||||
implementation("io.coil-kt:coil:$coil_version")
|
||||
implementation("io.coil-kt:coil-gif:$coil_version")
|
||||
implementation("io.coil-kt:coil-svg:$coil_version")
|
||||
implementation("io.coil-kt:coil-video:$coil_version")
|
||||
|
||||
// https://github.com/Baseflow/PhotoView/blob/master/LICENSE Apache v2.0
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
|
||||
implementation platform('com.google.firebase:firebase-bom:32.5.0')
|
||||
if (crashlyticsAvailable) {
|
||||
debugImplementation 'com.google.firebase:firebase-crashlytics-ndk'
|
||||
releaseWithCrashlyticsImplementation 'com.google.firebase:firebase-crashlytics-ndk'
|
||||
releaseCompileOnly 'com.google.firebase:firebase-crashlytics-ndk'
|
||||
} else {
|
||||
compileOnly 'com.google.firebase:firebase-crashlytics-ndk'
|
||||
}
|
||||
if (firebaseAvailable) {
|
||||
implementation 'com.google.firebase:firebase-messaging'
|
||||
}
|
||||
|
||||
implementation 'org.linphone:linphone-sdk-android:5.4+'
|
||||
|
||||
// Only enable leak canary prior to release
|
||||
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
|
||||
}
|
||||
|
||||
task generateContactsXml(type: Copy) {
|
||||
from 'contacts.xml'
|
||||
into "src/main/res/xml/"
|
||||
outputs.upToDateWhen { file('src/main/res/xml/contacts.xml').exists() }
|
||||
filter {
|
||||
line -> line
|
||||
.replaceAll('%%AUTO_GENERATED%%', 'This file has been automatically generated, do not edit or commit !')
|
||||
.replaceAll('%%PACKAGE_NAME%%', packageName)
|
||||
|
||||
}
|
||||
}
|
||||
project.tasks['preBuild'].dependsOn 'generateContactsXml'
|
||||
|
||||
ktlint {
|
||||
android = true
|
||||
ignoreFailures = true
|
||||
}
|
||||
|
||||
project.tasks['preBuild'].dependsOn 'ktlintFormat'
|
||||
|
||||
if (crashlyticsAvailable) {
|
||||
afterEvaluate {
|
||||
assembleReleaseWithCrashlytics.finalizedBy(uploadCrashlyticsSymbolFileReleaseWithCrashlytics)
|
||||
packageReleaseWithCrashlytics.finalizedBy(uploadCrashlyticsSymbolFileReleaseWithCrashlytics)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ContactsSource xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- %%AUTO_GENERATED%% -->
|
||||
<ContactsDataKind
|
||||
android:detailColumn="data3"
|
||||
android:detailSocialSummary="true"
|
||||
android:icon="@drawable/linphone_logo_tinted"
|
||||
android:mimeType="vnd.android.cursor.item/vnd.%%PACKAGE_NAME%%.provider.sip_address"
|
||||
android:summaryColumn="data2" />
|
||||
<!-- You can't use @string/linphone_address_mime_type above ! You have to hardcode it... -->
|
||||
</ContactsSource>
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
{
|
||||
"project_info": {
|
||||
"project_number": "929724111839",
|
||||
"firebase_url": "https://linphone-android-8a563.firebaseio.com",
|
||||
"project_id": "linphone-android-8a563",
|
||||
"storage_bucket": "linphone-android-8a563.appspot.com"
|
||||
},
|
||||
"client": [
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:929724111839:android:4662ea9a056188c4",
|
||||
"android_client_info": {
|
||||
"package_name": "org.linphone"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "929724111839-co5kffto4j7dets7oolvfv0056cvpfbl.apps.googleusercontent.com",
|
||||
"client_type": 1,
|
||||
"android_info": {
|
||||
"package_name": "org.linphone",
|
||||
"certificate_hash": "85463a95603f7b6331899b74b85d53d043dcd500"
|
||||
}
|
||||
},
|
||||
{
|
||||
"client_id": "929724111839-v5so1tcd65iil7dd7sde8jgii44h8luf.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCKrwWhkbA7Iy3wpEI8_ZvKOMp5jf6vV6A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"client_info": {
|
||||
"mobilesdk_app_id": "1:929724111839:android:3cf90ee1d2f8fcb6",
|
||||
"android_client_info": {
|
||||
"package_name": "org.linphone.debug"
|
||||
}
|
||||
},
|
||||
"oauth_client": [
|
||||
{
|
||||
"client_id": "929724111839-v5so1tcd65iil7dd7sde8jgii44h8luf.apps.googleusercontent.com",
|
||||
"client_type": 3
|
||||
}
|
||||
],
|
||||
"api_key": [
|
||||
{
|
||||
"current_key": "AIzaSyCKrwWhkbA7Iy3wpEI8_ZvKOMp5jf6vV6A"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"configuration_version": "1"
|
||||
}
|
||||
41
app/proguard-rules.pro
vendored
41
app/proguard-rules.pro
vendored
|
|
@ -1,41 +0,0 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
||||
-keep public class * extends androidx.fragment.app.Fragment { *; }
|
||||
-dontwarn com.google.errorprone.annotations.Immutable
|
||||
|
||||
# To prevent following errors:
|
||||
#ERROR: Missing classes detected while running R8. Please add the missing classes or apply additional keep rules that are generated in /builds/BC/public/linphone-android/app/build/outputs/mapping/release/missing_rules.txt.
|
||||
#ERROR: R8: Missing class org.bouncycastle.jsse.BCSSLParameters (referenced from: void okhttp3.internal.platform.BouncyCastlePlatform.configureTlsExtensions(javax.net.ssl.SSLSocket, java.lang.String, java.util.List) and 1 other context)
|
||||
#Missing class org.bouncycastle.jsse.BCSSLSocket (referenced from: void okhttp3.internal.platform.BouncyCastlePlatform.configureTlsExtensions(javax.net.ssl.SSLSocket, java.lang.String, java.util.List) and 5 other contexts)
|
||||
#Missing class org.bouncycastle.jsse.provider.BouncyCastleJsseProvider (referenced from: void okhttp3.internal.platform.BouncyCastlePlatform.<init>())
|
||||
#Missing class org.conscrypt.Conscrypt$Version (referenced from: boolean okhttp3.internal.platform.ConscryptPlatform$Companion.atLeastVersion(int, int, int))
|
||||
#Missing class org.conscrypt.Conscrypt (referenced from: boolean okhttp3.internal.platform.ConscryptPlatform$Companion.atLeastVersion(int, int, int) and 4 other contexts)
|
||||
#Missing class org.conscrypt.ConscryptHostnameVerifier (referenced from: okhttp3.internal.platform.ConscryptPlatform$DisabledHostnameVerifier)
|
||||
#Missing class org.openjsse.javax.net.ssl.SSLParameters (referenced from: void okhttp3.internal.platform.OpenJSSEPlatform.configureTlsExtensions(javax.net.ssl.SSLSocket, java.lang.String, java.util.List))
|
||||
#Missing class org.openjsse.javax.net.ssl.SSLSocket (referenced from: void okhttp3.internal.platform.OpenJSSEPlatform.configureTlsExtensions(javax.net.ssl.SSLSocket, java.lang.String, java.util.List) and 1 other context)
|
||||
#Missing class org.openjsse.net.ssl.OpenJSSE (referenced from: void okhttp3.internal.platform.OpenJSSEPlatform.<init>())
|
||||
#> Task :app:lintVitalAnalyzeRelease
|
||||
#FAILURE: Build failed with an exception.
|
||||
-dontwarn org.conscrypt.**
|
||||
-dontwarn org.bouncycastle.**
|
||||
-dontwarn org.openjsse.**
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- To be able to display contacts list & match calling/called numbers -->
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<!-- For in-app contact edition -->
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
||||
|
||||
<!-- Helps filling phone number and country code in assistant -->
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
|
||||
<!-- Needed for auto start at boot and to ensure the service won't be killed by OS while in call -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<!-- Starting Android 13 we need to ask notification permission -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
|
||||
<!-- Needed for full screen intent in incoming call notifications -->
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
<!-- To vibrate when pressing DTMF keys on numpad & incoming calls -->
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- Needed to attach file(s) in chat room fragment -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32"/>
|
||||
<!-- Starting Android 13 you need those 3 permissions instead (https://developer.android.com/about/versions/13/behavior-changes-13) -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
|
||||
<!-- Needed to shared downloaded files if setting is on -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
|
||||
<!-- Both permissions below are for contacts sync account, needed to store presence in native contact if enabled -->
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||
|
||||
<!-- Needed for Telecom Manager -->
|
||||
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
|
||||
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
|
||||
|
||||
<!-- Needed for overlay -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<!-- Needed to check current Do not disturb policy -->
|
||||
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
|
||||
|
||||
<!-- Needed for foreground service
|
||||
(https://developer.android.com/guide/components/foreground-services) -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<!-- Needed for Android 14
|
||||
https://developer.android.com/about/versions/14/behavior-changes-14#fgs-types -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<application
|
||||
android:name=".LinphoneApplication"
|
||||
android:allowBackup="false"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="${appLabel}"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:theme="@style/AppTheme"
|
||||
android:allowNativeHeapPointerTagging="false">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc"/>
|
||||
|
||||
<activity android:name=".activities.main.MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:theme="@style/AppSplashScreenTheme">
|
||||
<nav-graph android:value="@navigation/main_nav_graph" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW_LOCUS" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="application/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="application/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="${linphone_address_mime_type}" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<action android:name="android.intent.action.DIAL" />
|
||||
<action android:name="android.intent.action.CALL" />
|
||||
<action android:name="android.intent.action.CALL_BUTTON" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="tel" />
|
||||
<data android:scheme="sip" />
|
||||
<data android:scheme="sips" />
|
||||
<data android:scheme="linphone" />
|
||||
<data android:scheme="sip-linphone" />
|
||||
<data android:scheme="linphone-config" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<action android:name="android.intent.action.SENDTO" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="sms" />
|
||||
<data android:scheme="smsto" />
|
||||
<data android:scheme="mms" />
|
||||
<data android:scheme="mmsto" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".activities.assistant.AssistantActivity"
|
||||
android:windowSoftInputMode="adjustResize"/>
|
||||
|
||||
<activity android:name=".activities.voip.CallActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:turnScreenOn="true"
|
||||
android:showWhenLocked="true"
|
||||
android:resizeableActivity="true"
|
||||
android:supportsPictureInPicture="true" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.chat_bubble.ChatBubbleActivity"
|
||||
android:allowEmbedded="true"
|
||||
android:documentLaunchMode="always"
|
||||
android:resizeableActivity="true" />
|
||||
|
||||
<!-- Services -->
|
||||
|
||||
<service
|
||||
android:name=".core.CoreService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="phoneCall|camera|microphone|dataSync"
|
||||
android:stopWithTask="false"
|
||||
android:label="@string/app_name" />
|
||||
|
||||
<service
|
||||
android:name="org.linphone.core.tools.service.PushService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:stopWithTask="false"
|
||||
android:label="@string/app_name" />
|
||||
|
||||
<service android:name="org.linphone.core.tools.firebase.FirebaseMessaging"
|
||||
android:enabled="${firebaseServiceEnabled}"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".contact.DummySyncService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/sync_adapter" />
|
||||
<meta-data
|
||||
android:name="android.provider.CONTACTS_STRUCTURE"
|
||||
android:resource="@xml/contacts" />
|
||||
</service>
|
||||
|
||||
<service android:name=".contact.DummyAuthenticationService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.accounts.AccountAuthenticator"
|
||||
android:resource="@xml/authenticator" />
|
||||
</service>
|
||||
|
||||
<service android:name=".telecom.TelecomConnectionService"
|
||||
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.telecom.ConnectionService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<!-- Receivers -->
|
||||
|
||||
<receiver android:name=".core.CorePushReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="org.linphone.core.action.PUSH_RECEIVED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".notifications.NotificationBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver android:name=".core.BootReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- Providers -->
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${linphone_file_provider}"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config xmlns="http://www.linphone.org/xsds/lpconfig.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.linphone.org/xsds/lpconfig.xsd lpconfig.xsd">
|
||||
<section name="proxy_default_values">
|
||||
<entry name="avpf" overwrite="true">0</entry>
|
||||
<entry name="dial_escape_plus" overwrite="true">0</entry>
|
||||
<entry name="publish" overwrite="true">0</entry>
|
||||
<entry name="publish_expires" overwrite="true">-1</entry>
|
||||
<entry name="quality_reporting_collector" overwrite="true"></entry>
|
||||
<entry name="quality_reporting_enabled" overwrite="true">0</entry>
|
||||
<entry name="quality_reporting_interval" overwrite="true">0</entry>
|
||||
<entry name="reg_expires" overwrite="true">3600</entry>
|
||||
<entry name="reg_identity" overwrite="true"></entry>
|
||||
<entry name="reg_proxy" overwrite="true"></entry>
|
||||
<entry name="reg_route" overwrite="true"></entry>
|
||||
<entry name="reg_sendregister" overwrite="true">1</entry>
|
||||
<entry name="nat_policy_ref" overwrite="true"></entry>
|
||||
<entry name="realm" overwrite="true"></entry>
|
||||
<entry name="conference_factory_uri" overwrite="true"></entry>
|
||||
<entry name="audio_video_conference_factory_uri" overwrite="true"></entry>
|
||||
<entry name="push_notification_allowed" overwrite="true">0</entry>
|
||||
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">0</entry>
|
||||
<entry name="rtp_bundle" overwrite="true">0</entry>
|
||||
<entry name="lime_server_url" overwrite="true"></entry>
|
||||
</section>
|
||||
<section name="nat_policy_default_values">
|
||||
<entry name="stun_server" overwrite="true"></entry>
|
||||
<entry name="protocols" overwrite="true"></entry>
|
||||
</section>
|
||||
<section name="net">
|
||||
<entry name="friendlist_subscription_enabled" overwrite="true">0</entry>
|
||||
</section>
|
||||
<section name="assistant">
|
||||
<entry name="domain" overwrite="true"></entry>
|
||||
<entry name="algorithm" overwrite="true">MD5</entry>
|
||||
<entry name="password_max_length" overwrite="true">-1</entry>
|
||||
<entry name="password_min_length" overwrite="true">0</entry>
|
||||
<entry name="username_length" overwrite="true">-1</entry>
|
||||
<entry name="username_max_length" overwrite="true">128</entry>
|
||||
<entry name="username_min_length" overwrite="true">1</entry>
|
||||
<entry name="username_regex" overwrite="true">^[a-zA-Z0-9+_.\-]*$</entry>
|
||||
</section>
|
||||
</config>
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config xmlns="http://www.linphone.org/xsds/lpconfig.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.linphone.org/xsds/lpconfig.xsd lpconfig.xsd">
|
||||
<section name="proxy_default_values">
|
||||
<entry name="avpf" overwrite="true">1</entry>
|
||||
<entry name="dial_escape_plus" overwrite="true">0</entry>
|
||||
<entry name="publish" overwrite="true">1</entry>
|
||||
<entry name="publish_expires" overwrite="true">120</entry>
|
||||
<entry name="quality_reporting_collector" overwrite="true">sip:voip-metrics@sip.linphone.org;transport=tls</entry>
|
||||
<entry name="quality_reporting_enabled" overwrite="true">1</entry>
|
||||
<entry name="quality_reporting_interval" overwrite="true">180</entry>
|
||||
<entry name="reg_expires" overwrite="true">31536000</entry>
|
||||
<entry name="reg_identity" overwrite="true">sip:?@sip.linphone.org</entry>
|
||||
<entry name="reg_proxy" overwrite="true"><sip:sip.linphone.org;transport=tls></entry>
|
||||
<entry name="reg_route" overwrite="true"><sip:sip.linphone.org;transport=tls></entry>
|
||||
<entry name="reg_sendregister" overwrite="true">1</entry>
|
||||
<entry name="nat_policy_ref" overwrite="true">nat_policy_default_values</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="audio_video_conference_factory_uri" overwrite="true">sip:videoconference-factory@sip.linphone.org</entry>
|
||||
<entry name="push_notification_allowed" overwrite="true">1</entry>
|
||||
<entry name="cpim_in_basic_chat_rooms_enabled" overwrite="true">1</entry>
|
||||
<entry name="rtp_bundle" overwrite="true">1</entry>
|
||||
<entry name="lime_server_url" overwrite="true">https://lime.linphone.org/lime-server/lime-server.php</entry>
|
||||
</section>
|
||||
<section name="nat_policy_default_values">
|
||||
<entry name="stun_server" overwrite="true">stun.linphone.org</entry>
|
||||
<entry name="protocols" overwrite="true">stun,ice</entry>
|
||||
</section>
|
||||
<section name="net">
|
||||
<entry name="friendlist_subscription_enabled" overwrite="true">1</entry>
|
||||
</section>
|
||||
<section name="assistant">
|
||||
<entry name="domain" overwrite="true">sip.linphone.org</entry>
|
||||
<entry name="algorithm" overwrite="true">SHA-256</entry>
|
||||
<entry name="password_max_length" overwrite="true">-1</entry>
|
||||
<entry name="password_min_length" overwrite="true">1</entry>
|
||||
<entry name="username_length" overwrite="true">-1</entry>
|
||||
<entry name="username_max_length" overwrite="true">64</entry>
|
||||
<entry name="username_min_length" overwrite="true">1</entry>
|
||||
<entry name="username_regex" overwrite="true">^[a-z0-9+_.\-]*$</entry>
|
||||
</section>
|
||||
</config>
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
|
||||
## Start of default rc
|
||||
|
||||
[sip]
|
||||
contact="Linphone Android" <sip:linphone.android@unknown-host>
|
||||
use_info=0
|
||||
use_ipv6=1
|
||||
keepalive_period=30000
|
||||
sip_port=-1
|
||||
sip_tcp_port=-1
|
||||
sip_tls_port=-1
|
||||
media_encryption=none
|
||||
update_presence_model_timestamp_before_publish_expires_refresh=1
|
||||
|
||||
[net]
|
||||
#Because dynamic bitrate adaption can increase bitrate, we must allow "no limit"
|
||||
download_bw=0
|
||||
upload_bw=0
|
||||
|
||||
[video]
|
||||
size=vga
|
||||
|
||||
[app]
|
||||
tunnel=disabled
|
||||
auto_start=1
|
||||
record_aware=1
|
||||
|
||||
[tunnel]
|
||||
host=
|
||||
port=443
|
||||
|
||||
[misc]
|
||||
log_collection_upload_server_url=https://www.linphone.org:444/lft.php
|
||||
file_transfer_server_url=https://www.linphone.org:444/lft.php
|
||||
version_check_url_root=https://www.linphone.org/releases
|
||||
max_calls=10
|
||||
history_max_size=100
|
||||
conference_layout=1
|
||||
|
||||
[in-app-purchase]
|
||||
server_url=https://subscribe.linphone.org:444/inapp.php
|
||||
purchasable_items_ids=test_account_subscription
|
||||
|
||||
## End of default rc
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
|
||||
## Start of factory rc
|
||||
|
||||
# This file shall not contain path referencing package name, in order to be portable when app is renamed.
|
||||
# Paths to resources must be set from LinphoneManager, after creating LinphoneCore.
|
||||
|
||||
[net]
|
||||
mtu=1300
|
||||
force_ice_disablement=0
|
||||
|
||||
[rtp]
|
||||
accept_any_encryption=1
|
||||
|
||||
[sip]
|
||||
guess_hostname=1
|
||||
register_only_when_network_is_up=1
|
||||
auto_net_state_mon=1
|
||||
auto_answer_replacing_calls=1
|
||||
ping_with_options=0
|
||||
use_cpim=1
|
||||
zrtp_key_agreements_suites=MS_ZRTP_KEY_AGREEMENT_K255_KYB512
|
||||
chat_messages_aggregation_delay=1000
|
||||
chat_messages_aggregation=1
|
||||
|
||||
[sound]
|
||||
#remove this property for any application that is not Linphone public version itself
|
||||
ec_calibrator_cool_tones=1
|
||||
|
||||
[video]
|
||||
displaytype=MSAndroidTextureDisplay
|
||||
auto_resize_preview_to_keep_ratio=1
|
||||
max_conference_size=vga
|
||||
|
||||
[misc]
|
||||
enable_basic_to_client_group_chat_room_migration=0
|
||||
enable_simple_group_chat_message_state=0
|
||||
aggregate_imdn=1
|
||||
notify_each_friend_individually_when_presence_received=0
|
||||
|
||||
[app]
|
||||
activation_code_length=4
|
||||
prefer_basic_chat_room=1
|
||||
record_aware=1
|
||||
|
||||
[account_creator]
|
||||
backend=1
|
||||
# 1 means FlexiAPI, 0 is XMLRPC
|
||||
url=https://subscribe.linphone.org/api/
|
||||
# replace above URL by https://staging-subscribe.linphone.org/api/ for testing
|
||||
|
||||
[lime]
|
||||
lime_update_threshold=86400
|
||||
|
||||
## End of factory rc
|
||||
|
|
@ -1,143 +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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import coil.decode.GifDecoder
|
||||
import coil.decode.ImageDecoderDecoder
|
||||
import coil.decode.SvgDecoder
|
||||
import coil.decode.VideoFrameDecoder
|
||||
import coil.disk.DiskCache
|
||||
import coil.memory.MemoryCache
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.mediastream.Version
|
||||
|
||||
class LinphoneApplication : Application(), ImageLoaderFactory {
|
||||
companion object {
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
lateinit var corePreferences: CorePreferences
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
lateinit var coreContext: CoreContext
|
||||
|
||||
private fun createConfig(context: Context) {
|
||||
if (::corePreferences.isInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
Factory.instance().setLogCollectionPath(context.filesDir.absolutePath)
|
||||
Factory.instance().enableLogCollection(LogCollectionState.Enabled)
|
||||
|
||||
// For VFS
|
||||
Factory.instance().setCacheDir(context.cacheDir.absolutePath)
|
||||
|
||||
corePreferences = CorePreferences(context)
|
||||
corePreferences.copyAssetsFromPackage()
|
||||
|
||||
if (corePreferences.vfsEnabled) {
|
||||
CoreContext.activateVFS()
|
||||
}
|
||||
|
||||
val config = Factory.instance().createConfigWithFactory(
|
||||
corePreferences.configPath,
|
||||
corePreferences.factoryConfigPath
|
||||
)
|
||||
corePreferences.config = config
|
||||
|
||||
val appName = context.getString(R.string.app_name)
|
||||
Factory.instance().setLoggerDomain(appName)
|
||||
Factory.instance().enableLogcatLogs(corePreferences.logcatLogsOutput)
|
||||
if (corePreferences.debugLogs) {
|
||||
Factory.instance().loggingService.setLogLevel(LogLevel.Message)
|
||||
}
|
||||
|
||||
Log.i("[Application] Core config & preferences created")
|
||||
}
|
||||
|
||||
fun ensureCoreExists(
|
||||
context: Context,
|
||||
pushReceived: Boolean = false,
|
||||
service: CoreService? = null,
|
||||
useAutoStartDescription: Boolean = false,
|
||||
skipCoreStart: Boolean = false
|
||||
): Boolean {
|
||||
if (::coreContext.isInitialized && !coreContext.stopped) {
|
||||
Log.d("[Application] Skipping Core creation (push received? $pushReceived)")
|
||||
return false
|
||||
}
|
||||
|
||||
Log.i(
|
||||
"[Application] Core context is being created ${if (pushReceived) "from push" else ""}"
|
||||
)
|
||||
coreContext = CoreContext(
|
||||
context,
|
||||
corePreferences.config,
|
||||
service,
|
||||
useAutoStartDescription
|
||||
)
|
||||
if (!skipCoreStart) {
|
||||
coreContext.start()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun contextExists(): Boolean {
|
||||
return ::coreContext.isInitialized
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
val appName = getString(R.string.app_name)
|
||||
android.util.Log.i("[$appName]", "Application is being created")
|
||||
createConfig(applicationContext)
|
||||
Log.i("[Application] Created")
|
||||
}
|
||||
|
||||
override fun newImageLoader(): ImageLoader {
|
||||
return ImageLoader.Builder(this)
|
||||
.components {
|
||||
add(VideoFrameDecoder.Factory())
|
||||
add(SvgDecoder.Factory())
|
||||
if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) {
|
||||
add(ImageDecoderDecoder.Factory())
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
}
|
||||
.memoryCache {
|
||||
MemoryCache.Builder(this)
|
||||
.maxSizePercent(0.25)
|
||||
.build()
|
||||
}
|
||||
.diskCache {
|
||||
DiskCache.Builder()
|
||||
.directory(cacheDir.resolve("image_cache"))
|
||||
.maxSizePercent(0.02)
|
||||
.build()
|
||||
}
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,137 +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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.Display
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.ActivityNavigator
|
||||
import androidx.window.layout.FoldingFeature
|
||||
import androidx.window.layout.WindowInfoTracker
|
||||
import androidx.window.layout.WindowLayoutInfo
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.LinphoneApplication.Companion.ensureCoreExists
|
||||
import org.linphone.R
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
abstract class GenericActivity : AppCompatActivity() {
|
||||
private var timer: Timer? = null
|
||||
private var _isDestructionPending = false
|
||||
val isDestructionPending: Boolean
|
||||
get() = _isDestructionPending
|
||||
|
||||
open fun onLayoutChanges(foldingFeature: FoldingFeature?) { }
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Log.i("[Generic Activity] Ensuring Core exists")
|
||||
ensureCoreExists(applicationContext)
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
WindowInfoTracker
|
||||
.getOrCreate(this@GenericActivity)
|
||||
.windowLayoutInfo(this@GenericActivity)
|
||||
.collect { newLayoutInfo ->
|
||||
updateCurrentLayout(newLayoutInfo)
|
||||
}
|
||||
}
|
||||
|
||||
requestedOrientation = if (corePreferences.forcePortrait) {
|
||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
} else {
|
||||
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
|
||||
_isDestructionPending = false
|
||||
val nightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
val darkModeEnabled = corePreferences.darkMode
|
||||
when (nightMode) {
|
||||
Configuration.UI_MODE_NIGHT_NO, Configuration.UI_MODE_NIGHT_UNDEFINED -> {
|
||||
if (darkModeEnabled == 1) {
|
||||
// Force dark mode
|
||||
Log.w("[Generic Activity] Forcing night mode")
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
_isDestructionPending = true
|
||||
}
|
||||
}
|
||||
Configuration.UI_MODE_NIGHT_YES -> {
|
||||
if (darkModeEnabled == 0) {
|
||||
// Force light mode
|
||||
Log.w("[Generic Activity] Forcing day mode")
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
_isDestructionPending = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateScreenSize()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Remove service notification if it has been started by device boot
|
||||
coreContext.notificationsManager.stopForegroundNotificationIfPossible()
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
ActivityNavigator.applyPopAnimationsToPendingTransition(this)
|
||||
}
|
||||
|
||||
fun isTablet(): Boolean {
|
||||
return resources.getBoolean(R.bool.isTablet)
|
||||
}
|
||||
|
||||
private fun updateScreenSize() {
|
||||
val metrics = DisplayMetrics()
|
||||
val display: Display = windowManager.defaultDisplay
|
||||
display.getRealMetrics(metrics)
|
||||
val screenWidth = metrics.widthPixels.toFloat()
|
||||
val screenHeight = metrics.heightPixels.toFloat()
|
||||
coreContext.screenWidth = screenWidth
|
||||
coreContext.screenHeight = screenHeight
|
||||
}
|
||||
|
||||
private fun updateCurrentLayout(newLayoutInfo: WindowLayoutInfo) {
|
||||
if (newLayoutInfo.displayFeatures.isEmpty()) {
|
||||
onLayoutChanges(null)
|
||||
} else {
|
||||
for (feature in newLayoutInfo.displayFeatures) {
|
||||
val foldingFeature = feature as? FoldingFeature
|
||||
if (foldingFeature != null) {
|
||||
onLayoutChanges(foldingFeature)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,185 +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
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.view.doOnPreDraw
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
abstract class GenericFragment<T : ViewDataBinding> : Fragment() {
|
||||
companion object {
|
||||
val emptyFragmentsIds = arrayListOf(
|
||||
R.id.emptyChatFragment,
|
||||
R.id.emptyContactFragment,
|
||||
R.id.emptySettingsFragment,
|
||||
R.id.emptyCallHistoryFragment
|
||||
)
|
||||
}
|
||||
|
||||
private var _binding: T? = null
|
||||
protected val binding get() = _binding!!
|
||||
|
||||
protected var useMaterialSharedAxisXForwardAnimation = true
|
||||
|
||||
protected lateinit var sharedViewModel: SharedMainViewModel
|
||||
|
||||
protected fun isSharedViewModelInitialized(): Boolean {
|
||||
return ::sharedViewModel.isInitialized
|
||||
}
|
||||
|
||||
protected fun isBindingAvailable(): Boolean {
|
||||
return _binding != null
|
||||
}
|
||||
|
||||
private fun getFragmentRealClassName(): String {
|
||||
return this.javaClass.name
|
||||
}
|
||||
|
||||
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
try {
|
||||
val navController = findNavController()
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} handleOnBackPressed")
|
||||
if (!navController.popBackStack()) {
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} couldn't pop")
|
||||
if (!navController.navigateUp()) {
|
||||
Log.d(
|
||||
"[Generic Fragment] ${getFragmentRealClassName()} couldn't navigate up"
|
||||
)
|
||||
// Disable this callback & start a new back press event
|
||||
isEnabled = false
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.e(
|
||||
"[Generic Fragment] ${getFragmentRealClassName()}.handleOnBackPressed() Can't go back: $ise"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getLayoutId(): Int
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
sharedViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedMainViewModel::class.java]
|
||||
}
|
||||
|
||||
sharedViewModel.isSlidingPaneSlideable.observe(viewLifecycleOwner) {
|
||||
Log.d(
|
||||
"[Generic Fragment] ${getFragmentRealClassName()} shared main VM sliding pane has changed"
|
||||
)
|
||||
onBackPressedCallback.isEnabled = backPressedCallBackEnabled()
|
||||
}
|
||||
|
||||
_binding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false)
|
||||
return _binding!!.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
if (useMaterialSharedAxisXForwardAnimation && corePreferences.enableAnimations) {
|
||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||
exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
||||
|
||||
postponeEnterTransition()
|
||||
binding.root.doOnPreDraw { startPostponedEnterTransition() }
|
||||
}
|
||||
|
||||
setupBackPressCallback()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
|
||||
onBackPressedCallback.remove()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
protected fun goBack() {
|
||||
try {
|
||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
||||
} catch (ise: IllegalStateException) {
|
||||
Log.w("[Generic Fragment] ${getFragmentRealClassName()}.goBack() can't go back: $ise")
|
||||
onBackPressedCallback.handleOnBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupBackPressCallback() {
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} setupBackPressCallback")
|
||||
|
||||
val backButton = binding.root.findViewById<ImageView>(R.id.back)
|
||||
if (backButton != null) {
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} found back button")
|
||||
// If popping navigation back stack entry would bring us to an "empty" fragment
|
||||
// then don't do it if sliding pane layout isn't "flat"
|
||||
onBackPressedCallback.isEnabled = backPressedCallBackEnabled()
|
||||
backButton.setOnClickListener { goBack() }
|
||||
} else {
|
||||
onBackPressedCallback.isEnabled = false
|
||||
}
|
||||
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
viewLifecycleOwner,
|
||||
onBackPressedCallback
|
||||
)
|
||||
}
|
||||
|
||||
private fun backPressedCallBackEnabled(): Boolean {
|
||||
// This allow to navigate a SlidingPane child nav graph.
|
||||
// This only concerns fragments for which the nav graph is inside a SlidingPane layout.
|
||||
// In our case it's all graphs except the main one.
|
||||
if (findNavController().graph.id == R.id.main_nav_graph_xml) return false
|
||||
|
||||
val isSlidingPaneFlat = sharedViewModel.isSlidingPaneSlideable.value == false
|
||||
Log.d(
|
||||
"[Generic Fragment] ${getFragmentRealClassName()} isSlidingPaneFlat ? $isSlidingPaneFlat"
|
||||
)
|
||||
val isPreviousFragmentEmpty = findNavController().previousBackStackEntry?.destination?.id in emptyFragmentsIds
|
||||
Log.d(
|
||||
"[Generic Fragment] ${getFragmentRealClassName()} isPreviousFragmentEmpty ? $isPreviousFragmentEmpty"
|
||||
)
|
||||
val popBackStack = isSlidingPaneFlat || !isPreviousFragmentEmpty
|
||||
Log.d("[Generic Fragment] ${getFragmentRealClassName()} popBackStack ? $popBackStack")
|
||||
return popBackStack
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,87 +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
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.os.PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
abstract class ProximitySensorActivity : GenericActivity() {
|
||||
private lateinit var proximityWakeLock: PowerManager.WakeLock
|
||||
private var proximitySensorEnabled = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
if (!powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
|
||||
Log.w(
|
||||
"[Proximity Sensor Activity] PROXIMITY_SCREEN_OFF_WAKE_LOCK isn't supported on this device!"
|
||||
)
|
||||
}
|
||||
|
||||
proximityWakeLock = powerManager.newWakeLock(
|
||||
PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
|
||||
"$packageName;proximity_sensor"
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
enableProximitySensor(false)
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
enableProximitySensor(false)
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
protected fun enableProximitySensor(enable: Boolean) {
|
||||
if (enable) {
|
||||
if (!proximitySensorEnabled) {
|
||||
Log.i(
|
||||
"[Proximity Sensor Activity] Enabling proximity sensor (turning screen OFF when wake lock is acquired)"
|
||||
)
|
||||
if (!proximityWakeLock.isHeld) {
|
||||
Log.i("[Proximity Sensor Activity] Acquiring PROXIMITY_SCREEN_OFF_WAKE_LOCK")
|
||||
proximityWakeLock.acquire()
|
||||
}
|
||||
proximitySensorEnabled = true
|
||||
}
|
||||
} else {
|
||||
if (proximitySensorEnabled) {
|
||||
Log.i(
|
||||
"[Proximity Sensor Activity] Disabling proximity sensor (turning screen ON when wake lock is released)"
|
||||
)
|
||||
if (proximityWakeLock.isHeld) {
|
||||
Log.i(
|
||||
"[Proximity Sensor Activity] Asking to release PROXIMITY_SCREEN_OFF_WAKE_LOCK next time sensor detects no proximity"
|
||||
)
|
||||
proximityWakeLock.release(RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY)
|
||||
}
|
||||
proximitySensorEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +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
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
interface SnackBarActivity {
|
||||
fun showSnackBar(@StringRes resourceId: Int)
|
||||
fun showSnackBar(@StringRes resourceId: Int, action: Int, listener: () -> Unit)
|
||||
fun showSnackBar(message: String)
|
||||
}
|
||||
|
|
@ -1,65 +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.assistant
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericActivity
|
||||
import org.linphone.activities.SnackBarActivity
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
|
||||
class AssistantActivity : GenericActivity(), SnackBarActivity {
|
||||
private lateinit var sharedViewModel: SharedAssistantViewModel
|
||||
private lateinit var coordinator: CoordinatorLayout
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.assistant_activity)
|
||||
|
||||
sharedViewModel = ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
|
||||
coordinator = findViewById(R.id.coordinator)
|
||||
|
||||
corePreferences.firstStart = false
|
||||
}
|
||||
|
||||
override fun showSnackBar(@StringRes resourceId: Int) {
|
||||
Snackbar.make(coordinator, resourceId, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun showSnackBar(@StringRes resourceId: Int, action: Int, listener: () -> Unit) {
|
||||
Snackbar
|
||||
.make(findViewById(R.id.coordinator), resourceId, Snackbar.LENGTH_LONG)
|
||||
.setAction(action) {
|
||||
listener()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun showSnackBar(message: String) {
|
||||
Snackbar.make(coordinator, message, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,99 +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.assistant.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import android.widget.TextView
|
||||
import kotlin.collections.ArrayList
|
||||
import org.linphone.R
|
||||
import org.linphone.core.DialPlan
|
||||
import org.linphone.core.Factory
|
||||
|
||||
class CountryPickerAdapter : BaseAdapter(), Filterable {
|
||||
private var countries: ArrayList<DialPlan>
|
||||
|
||||
init {
|
||||
val dialPlans = Factory.instance().dialPlans
|
||||
countries = arrayListOf()
|
||||
countries.addAll(dialPlans)
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view: View = convertView ?: LayoutInflater.from(parent.context).inflate(
|
||||
R.layout.assistant_country_picker_cell,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
val dialPlan: DialPlan = countries[position]
|
||||
|
||||
val name = view.findViewById<TextView>(R.id.country_name)
|
||||
name.text = dialPlan.country
|
||||
|
||||
val dialCode = view.findViewById<TextView>(R.id.country_prefix)
|
||||
dialCode.text = String.format("(%s)", dialPlan.countryCallingCode)
|
||||
|
||||
view.tag = dialPlan
|
||||
return view
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): DialPlan {
|
||||
return countries[position]
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return countries.size
|
||||
}
|
||||
|
||||
override fun getFilter(): Filter {
|
||||
return object : Filter() {
|
||||
override fun performFiltering(constraint: CharSequence): FilterResults {
|
||||
val filteredCountries = arrayListOf<DialPlan>()
|
||||
for (dialPlan in Factory.instance().dialPlans) {
|
||||
if (dialPlan.country.contains(constraint, ignoreCase = true) ||
|
||||
dialPlan.countryCallingCode.contains(constraint)
|
||||
) {
|
||||
filteredCountries.add(dialPlan)
|
||||
}
|
||||
}
|
||||
val filterResults = FilterResults()
|
||||
filterResults.values = filteredCountries
|
||||
return filterResults
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun publishResults(
|
||||
constraint: CharSequence,
|
||||
results: FilterResults
|
||||
) {
|
||||
countries = results.values as ArrayList<DialPlan>
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,91 +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.assistant.fragments
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.viewmodels.AbstractPhoneViewModel
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.PermissionHelper
|
||||
import org.linphone.utils.PhoneNumberUtils
|
||||
|
||||
abstract class AbstractPhoneFragment<T : ViewDataBinding> : GenericFragment<T>() {
|
||||
companion object {
|
||||
const val READ_PHONE_STATE_PERMISSION_REQUEST_CODE = 0
|
||||
}
|
||||
|
||||
abstract val viewModel: AbstractPhoneViewModel
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == READ_PHONE_STATE_PERMISSION_REQUEST_CODE) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i("[Assistant] READ_PHONE_STATE/READ_PHONE_NUMBERS permission granted")
|
||||
updateFromDeviceInfo()
|
||||
} else {
|
||||
Log.w("[Assistant] READ_PHONE_STATE/READ_PHONE_NUMBERS permission denied")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun checkPermissions() {
|
||||
// Only ask for phone number related permission on devices that have TELEPHONY feature && if push notifications are available
|
||||
if (requireContext().packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY) && LinphoneUtils.isPushNotificationAvailable()) {
|
||||
if (!PermissionHelper.get().hasReadPhoneStateOrPhoneNumbersPermission()) {
|
||||
Log.i("[Assistant] Asking for READ_PHONE_STATE/READ_PHONE_NUMBERS permission")
|
||||
Compatibility.requestReadPhoneStateOrNumbersPermission(
|
||||
this,
|
||||
READ_PHONE_STATE_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
} else {
|
||||
updateFromDeviceInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFromDeviceInfo() {
|
||||
val phoneNumber = PhoneNumberUtils.getDevicePhoneNumber(requireContext())
|
||||
val dialPlan = PhoneNumberUtils.getDialPlanForCurrentCountry(requireContext())
|
||||
viewModel.updateFromPhoneNumberAndOrDialPlan(phoneNumber, dialPlan)
|
||||
}
|
||||
|
||||
protected fun showPhoneNumberInfoDialog() {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(getString(R.string.assistant_phone_number_info_title))
|
||||
.setMessage(
|
||||
getString(R.string.assistant_phone_number_link_info_content) + "\n" +
|
||||
getString(
|
||||
R.string.assistant_phone_number_link_info_content_already_account
|
||||
)
|
||||
)
|
||||
.setNegativeButton(getString(R.string.dialog_ok), null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,142 +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.assistant.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.AccountLoginViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.AccountLoginViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||
import org.linphone.activities.navigateToPhoneAccountValidation
|
||||
import org.linphone.databinding.AssistantAccountLoginFragmentBinding
|
||||
import org.linphone.utils.DialogUtils
|
||||
|
||||
class AccountLoginFragment : AbstractPhoneFragment<AssistantAccountLoginFragmentBinding>() {
|
||||
override lateinit var viewModel: AccountLoginViewModel
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_account_login_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
AccountLoginViewModelFactory(sharedAssistantViewModel.getAccountCreator())
|
||||
)[AccountLoginViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setInfoClickListener {
|
||||
showPhoneNumberInfoDialog()
|
||||
}
|
||||
|
||||
binding.setSelectCountryClickListener {
|
||||
val countryPickerFragment = CountryPickerFragment()
|
||||
countryPickerFragment.listener = viewModel
|
||||
countryPickerFragment.show(childFragmentManager, "CountryPicker")
|
||||
}
|
||||
|
||||
binding.setForgotPasswordClickListener {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.data = Uri.parse(getString(R.string.assistant_forgotten_password_link))
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
viewModel.prefix.observe(viewLifecycleOwner) { internationalPrefix ->
|
||||
viewModel.getCountryNameFromPrefix(internationalPrefix)
|
||||
}
|
||||
|
||||
viewModel.goToSmsValidationEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val args = Bundle()
|
||||
args.putBoolean("IsLogin", true)
|
||||
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
|
||||
navigateToPhoneAccountValidation(args)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
coreContext.newAccountConfigured(true)
|
||||
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
navigateToEchoCancellerCalibration()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.invalidCredentialsEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val dialogViewModel =
|
||||
DialogViewModel(getString(R.string.assistant_error_invalid_credentials))
|
||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||
|
||||
dialogViewModel.showCancelButton {
|
||||
viewModel.removeInvalidProxyConfig()
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialogViewModel.showDeleteButton(
|
||||
{
|
||||
viewModel.continueEvenIfInvalidCredentials()
|
||||
dialog.dismiss()
|
||||
},
|
||||
getString(R.string.assistant_continue_even_if_credentials_invalid)
|
||||
)
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,87 +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.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.*
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.adapters.CountryPickerAdapter
|
||||
import org.linphone.core.DialPlan
|
||||
import org.linphone.databinding.AssistantCountryPickerFragmentBinding
|
||||
|
||||
class CountryPickerFragment : DialogFragment() {
|
||||
private var _binding: AssistantCountryPickerFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var adapter: CountryPickerAdapter
|
||||
|
||||
var listener: CountryPickedListener? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NO_TITLE, R.style.assistant_country_dialog_style)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
_binding = AssistantCountryPickerFragmentBinding.inflate(inflater, container, false)
|
||||
|
||||
adapter = CountryPickerAdapter()
|
||||
binding.countryList.adapter = adapter
|
||||
|
||||
binding.countryList.setOnItemClickListener { _, _, position, _ ->
|
||||
if (position >= 0 && position < adapter.count) {
|
||||
val dialPlan = adapter.getItem(position)
|
||||
listener?.onCountryClicked(dialPlan)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.searchCountry.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
adapter.filter.filter(s)
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { }
|
||||
})
|
||||
|
||||
binding.setCancelClickListener {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
interface CountryPickedListener {
|
||||
fun onCountryClicked(dialPlan: DialPlan)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,87 +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.assistant.fragments
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.viewmodels.EchoCancellerCalibrationViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantEchoCancellerCalibrationFragmentBinding
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class EchoCancellerCalibrationFragment : GenericFragment<AssistantEchoCancellerCalibrationFragmentBinding>() {
|
||||
companion object {
|
||||
const val RECORD_AUDIO_PERMISSION_REQUEST_CODE = 0
|
||||
}
|
||||
|
||||
private lateinit var viewModel: EchoCancellerCalibrationViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_echo_canceller_calibration_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
viewModel = ViewModelProvider(this)[EchoCancellerCalibrationViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.echoCalibrationTerminated.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
|
||||
if (!PermissionHelper.required(requireContext()).hasRecordAudioPermission()) {
|
||||
Log.i("[Echo Canceller Calibration] Asking for RECORD_AUDIO permission")
|
||||
requestPermissions(
|
||||
arrayOf(android.Manifest.permission.RECORD_AUDIO),
|
||||
RECORD_AUDIO_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
} else {
|
||||
viewModel.startEchoCancellerCalibration()
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == RECORD_AUDIO_PERMISSION_REQUEST_CODE) {
|
||||
val granted =
|
||||
grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
if (granted) {
|
||||
Log.i("[Echo Canceller Calibration] RECORD_AUDIO permission granted")
|
||||
viewModel.startEchoCancellerCalibration()
|
||||
} else {
|
||||
Log.w("[Echo Canceller Calibration] RECORD_AUDIO permission denied")
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,71 +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.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.EmailAccountCreationViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.EmailAccountCreationViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.navigateToEmailAccountValidation
|
||||
import org.linphone.databinding.AssistantEmailAccountCreationFragmentBinding
|
||||
|
||||
class EmailAccountCreationFragment : GenericFragment<AssistantEmailAccountCreationFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: EmailAccountCreationViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_email_account_creation_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
EmailAccountCreationViewModelFactory(sharedAssistantViewModel.getAccountCreator())
|
||||
)[EmailAccountCreationViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.goToEmailValidationEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
navigateToEmailAccountValidation()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,81 +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.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.*
|
||||
import org.linphone.activities.navigateToAccountLinking
|
||||
import org.linphone.databinding.AssistantEmailAccountValidationFragmentBinding
|
||||
|
||||
class EmailAccountValidationFragment : GenericFragment<AssistantEmailAccountValidationFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: EmailAccountValidationViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_email_account_validation_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
EmailAccountValidationViewModelFactory(sharedAssistantViewModel.getAccountCreator())
|
||||
)[EmailAccountValidationViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
coreContext.newAccountConfigured(true)
|
||||
|
||||
if (!corePreferences.hideLinkPhoneNumber) {
|
||||
val args = Bundle()
|
||||
args.putBoolean("AllowSkip", true)
|
||||
args.putString("Username", viewModel.accountCreator.username)
|
||||
args.putString("Password", viewModel.accountCreator.password)
|
||||
navigateToAccountLinking(args)
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,108 +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.assistant.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.GenericLoginViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.GenericLoginViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||
import org.linphone.databinding.AssistantGenericAccountLoginFragmentBinding
|
||||
import org.linphone.utils.DialogUtils
|
||||
|
||||
class GenericAccountLoginFragment : GenericFragment<AssistantGenericAccountLoginFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: GenericLoginViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_generic_account_login_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
GenericLoginViewModelFactory(sharedAssistantViewModel.getAccountCreator(true))
|
||||
)[GenericLoginViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val isLinphoneAccount = viewModel.domain.value.orEmpty() == corePreferences.defaultDomain
|
||||
coreContext.newAccountConfigured(isLinphoneAccount)
|
||||
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
navigateToEchoCancellerCalibration()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.invalidCredentialsEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val dialogViewModel =
|
||||
DialogViewModel(getString(R.string.assistant_error_invalid_credentials))
|
||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||
|
||||
dialogViewModel.showCancelButton {
|
||||
viewModel.removeInvalidProxyConfig()
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialogViewModel.showDeleteButton(
|
||||
{
|
||||
viewModel.continueEvenIfInvalidCredentials()
|
||||
dialog.dismiss()
|
||||
},
|
||||
getString(R.string.assistant_continue_even_if_credentials_invalid)
|
||||
)
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
/*
|
||||
* 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2023 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.databinding.AssistantNoPushWarningFragmentBinding
|
||||
|
||||
class NoPushWarningFragment : GenericFragment<AssistantNoPushWarningFragmentBinding>() {
|
||||
override fun getLayoutId(): Int = R.layout.assistant_no_push_warning_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
}
|
||||
}
|
||||
|
|
@ -1,90 +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.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.PhoneAccountCreationViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.PhoneAccountCreationViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.navigateToPhoneAccountValidation
|
||||
import org.linphone.databinding.AssistantPhoneAccountCreationFragmentBinding
|
||||
|
||||
class PhoneAccountCreationFragment :
|
||||
AbstractPhoneFragment<AssistantPhoneAccountCreationFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
override lateinit var viewModel: PhoneAccountCreationViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_phone_account_creation_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
PhoneAccountCreationViewModelFactory(sharedAssistantViewModel.getAccountCreator())
|
||||
)[PhoneAccountCreationViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setInfoClickListener {
|
||||
showPhoneNumberInfoDialog()
|
||||
}
|
||||
|
||||
binding.setSelectCountryClickListener {
|
||||
val countryPickerFragment = CountryPickerFragment()
|
||||
countryPickerFragment.listener = viewModel
|
||||
countryPickerFragment.show(childFragmentManager, "CountryPicker")
|
||||
}
|
||||
|
||||
viewModel.prefix.observe(viewLifecycleOwner) { internationalPrefix ->
|
||||
viewModel.getCountryNameFromPrefix(internationalPrefix)
|
||||
}
|
||||
|
||||
viewModel.goToSmsValidationEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val args = Bundle()
|
||||
args.putBoolean("IsCreation", true)
|
||||
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
|
||||
navigateToPhoneAccountValidation(args)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as AssistantActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,113 +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.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.SnackBarActivity
|
||||
import org.linphone.activities.assistant.viewmodels.*
|
||||
import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||
import org.linphone.activities.navigateToPhoneAccountValidation
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantPhoneAccountLinkingFragmentBinding
|
||||
|
||||
class PhoneAccountLinkingFragment : AbstractPhoneFragment<AssistantPhoneAccountLinkingFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
override lateinit var viewModel: PhoneAccountLinkingViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_phone_account_linking_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
val accountCreator = sharedAssistantViewModel.getAccountCreator()
|
||||
viewModel = ViewModelProvider(this, PhoneAccountLinkingViewModelFactory(accountCreator))[PhoneAccountLinkingViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
val username = arguments?.getString("Username")
|
||||
Log.i("[Phone Account Linking] username to link is $username")
|
||||
viewModel.username.value = username
|
||||
|
||||
val password = arguments?.getString("Password")
|
||||
accountCreator.password = password
|
||||
|
||||
val ha1 = arguments?.getString("HA1")
|
||||
accountCreator.ha1 = ha1
|
||||
|
||||
val allowSkip = arguments?.getBoolean("AllowSkip", false)
|
||||
viewModel.allowSkip.value = allowSkip
|
||||
|
||||
binding.setInfoClickListener {
|
||||
showPhoneNumberInfoDialog()
|
||||
}
|
||||
|
||||
binding.setSelectCountryClickListener {
|
||||
val countryPickerFragment = CountryPickerFragment()
|
||||
countryPickerFragment.listener = viewModel
|
||||
countryPickerFragment.show(childFragmentManager, "CountryPicker")
|
||||
}
|
||||
|
||||
viewModel.prefix.observe(viewLifecycleOwner) { internationalPrefix ->
|
||||
viewModel.getCountryNameFromPrefix(internationalPrefix)
|
||||
}
|
||||
|
||||
viewModel.goToSmsValidationEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val args = Bundle()
|
||||
args.putBoolean("IsLinking", true)
|
||||
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
|
||||
navigateToPhoneAccountValidation(args)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
navigateToEchoCancellerCalibration()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as SnackBarActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +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.assistant.fragments
|
||||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context.CLIPBOARD_SERVICE
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.SnackBarActivity
|
||||
import org.linphone.activities.assistant.viewmodels.PhoneAccountValidationViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.PhoneAccountValidationViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.navigateToAccountSettings
|
||||
import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantPhoneAccountValidationFragmentBinding
|
||||
|
||||
class PhoneAccountValidationFragment : GenericFragment<AssistantPhoneAccountValidationFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: PhoneAccountValidationViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_phone_account_validation_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
PhoneAccountValidationViewModelFactory(sharedAssistantViewModel.getAccountCreator())
|
||||
)[PhoneAccountValidationViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.phoneNumber.value = arguments?.getString("PhoneNumber")
|
||||
viewModel.isLogin.value = arguments?.getBoolean("IsLogin", false)
|
||||
viewModel.isCreation.value = arguments?.getBoolean("IsCreation", false)
|
||||
viewModel.isLinking.value = arguments?.getBoolean("IsLinking", false)
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
when {
|
||||
viewModel.isLogin.value == true || viewModel.isCreation.value == true -> {
|
||||
coreContext.newAccountConfigured(true)
|
||||
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
navigateToEchoCancellerCalibration()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
viewModel.isLinking.value == true -> {
|
||||
if (findNavController().graph.id == R.id.settings_nav_graph_xml) {
|
||||
val args = Bundle()
|
||||
args.putString(
|
||||
"Identity",
|
||||
"sip:${viewModel.accountCreator.username}@${viewModel.accountCreator.domain}"
|
||||
)
|
||||
navigateToAccountSettings(args)
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { message ->
|
||||
(requireActivity() as SnackBarActivity).showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
// This won't work starting Android 10 as clipboard access is denied unless app has focus,
|
||||
// which won't be the case when the SMS arrives unless it is added into clipboard from a notification
|
||||
val clipboard = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.addPrimaryClipChangedListener {
|
||||
val data = clipboard.primaryClip
|
||||
if (data != null && data.itemCount > 0) {
|
||||
val clip = data.getItemAt(0).text.toString()
|
||||
if (clip.length == 4) {
|
||||
Log.i(
|
||||
"[Assistant] [Phone Account Validation] Found 4 digits as primary clip in clipboard, using it and clear it"
|
||||
)
|
||||
viewModel.code.value = clip
|
||||
Compatibility.clearClipboard(clipboard)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,112 +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.assistant.fragments
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.viewmodels.QrCodeViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantQrCodeFragmentBinding
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class QrCodeFragment : GenericFragment<AssistantQrCodeFragmentBinding>() {
|
||||
companion object {
|
||||
const val CAMERA_PERMISSION_REQUEST_CODE = 0
|
||||
}
|
||||
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: QrCodeViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_qr_code_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this)[QrCodeViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.qrCodeFoundEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { url ->
|
||||
sharedAssistantViewModel.remoteProvisioningUrl.value = url
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
viewModel.setBackCamera()
|
||||
|
||||
if (!PermissionHelper.required(requireContext()).hasCameraPermission()) {
|
||||
Log.i("[QR Code] Asking for CAMERA permission")
|
||||
requestPermissions(
|
||||
arrayOf(android.Manifest.permission.CAMERA),
|
||||
CAMERA_PERMISSION_REQUEST_CODE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
coreContext.core.nativePreviewWindowId = binding.qrCodeCaptureTexture
|
||||
coreContext.core.isQrcodeVideoPreviewEnabled = true
|
||||
coreContext.core.isVideoPreviewEnabled = true
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
coreContext.core.nativePreviewWindowId = null
|
||||
coreContext.core.isQrcodeVideoPreviewEnabled = false
|
||||
coreContext.core.isVideoPreviewEnabled = false
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
|
||||
val granted =
|
||||
grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
if (granted) {
|
||||
Log.i("[QR Code] CAMERA permission granted")
|
||||
coreContext.core.reloadVideoDevices()
|
||||
viewModel.setBackCamera()
|
||||
} else {
|
||||
Log.w("[QR Code] CAMERA permission denied")
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +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.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.RemoteProvisioningViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.activities.navigateToEchoCancellerCalibration
|
||||
import org.linphone.activities.navigateToQrCode
|
||||
import org.linphone.databinding.AssistantRemoteProvisioningFragmentBinding
|
||||
|
||||
class RemoteProvisioningFragment : GenericFragment<AssistantRemoteProvisioningFragmentBinding>() {
|
||||
private lateinit var sharedAssistantViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: RemoteProvisioningViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_remote_provisioning_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
sharedAssistantViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[SharedAssistantViewModel::class.java]
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(this)[RemoteProvisioningViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setQrCodeClickListener {
|
||||
navigateToQrCode()
|
||||
}
|
||||
|
||||
viewModel.fetchSuccessfulEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { success ->
|
||||
if (success) {
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
navigateToEchoCancellerCalibration()
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
} else {
|
||||
val activity = requireActivity() as AssistantActivity
|
||||
activity.showSnackBar(R.string.assistant_remote_provisioning_failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.urlToFetch.value = sharedAssistantViewModel.remoteProvisioningUrl.value ?: coreContext.core.provisioningUri
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
|
||||
if (::sharedAssistantViewModel.isInitialized) {
|
||||
sharedAssistantViewModel.remoteProvisioningUrl.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +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.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericFragment
|
||||
import org.linphone.databinding.AssistantTopBarFragmentBinding
|
||||
|
||||
class TopBarFragment : GenericFragment<AssistantTopBarFragmentBinding>() {
|
||||
override fun getLayoutId(): Int = R.layout.assistant_top_bar_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
useMaterialSharedAxisXForwardAnimation = false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,170 +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.assistant.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.ClickableSpan
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import java.util.UnknownFormatConversionException
|
||||
import java.util.regex.Pattern
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.*
|
||||
import org.linphone.activities.assistant.viewmodels.WelcomeViewModel
|
||||
import org.linphone.activities.navigateToAccountLogin
|
||||
import org.linphone.activities.navigateToEmailAccountCreation
|
||||
import org.linphone.activities.navigateToRemoteProvisioning
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantWelcomeFragmentBinding
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class WelcomeFragment : GenericFragment<AssistantWelcomeFragmentBinding>() {
|
||||
private lateinit var viewModel: WelcomeViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.assistant_welcome_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
viewModel = ViewModelProvider(this)[WelcomeViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setCreateAccountClickListener {
|
||||
if (LinphoneUtils.isPushNotificationAvailable()) {
|
||||
Log.i("[Assistant] Core says push notifications are available")
|
||||
val deviceHasTelephonyFeature = coreContext.context.packageManager.hasSystemFeature(
|
||||
PackageManager.FEATURE_TELEPHONY
|
||||
)
|
||||
if (!deviceHasTelephonyFeature) {
|
||||
Log.i(
|
||||
"[Assistant] Device doesn't have TELEPHONY feature, showing email based account creation"
|
||||
)
|
||||
navigateToEmailAccountCreation()
|
||||
} else {
|
||||
Log.i(
|
||||
"[Assistant] Device has TELEPHONY feature, showing phone based account creation"
|
||||
)
|
||||
navigateToPhoneAccountCreation()
|
||||
}
|
||||
} else {
|
||||
Log.w(
|
||||
"[Assistant] Failed to get push notification info, showing warning instead of phone based account creation"
|
||||
)
|
||||
navigateToNoPushWarning()
|
||||
}
|
||||
}
|
||||
|
||||
binding.setAccountLoginClickListener {
|
||||
navigateToAccountLogin()
|
||||
}
|
||||
|
||||
binding.setGenericAccountLoginClickListener {
|
||||
navigateToGenericLoginWarning()
|
||||
}
|
||||
|
||||
binding.setRemoteProvisioningClickListener {
|
||||
navigateToRemoteProvisioning()
|
||||
}
|
||||
|
||||
viewModel.termsAndPrivacyAccepted.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
if (it) corePreferences.readAndAgreeTermsAndPrivacy = true
|
||||
}
|
||||
|
||||
setUpTermsAndPrivacyLinks()
|
||||
}
|
||||
|
||||
private fun setUpTermsAndPrivacyLinks() {
|
||||
val terms = getString(R.string.assistant_general_terms)
|
||||
val privacy = getString(R.string.assistant_privacy_policy)
|
||||
|
||||
val label = try {
|
||||
getString(
|
||||
R.string.assistant_read_and_agree_terms,
|
||||
terms,
|
||||
privacy
|
||||
)
|
||||
} catch (e: UnknownFormatConversionException) {
|
||||
Log.e("[Welcome] Wrong R.string.assistant_read_and_agree_terms format!")
|
||||
"I accept Belledonne Communications' terms of use and privacy policy"
|
||||
}
|
||||
val spannable = SpannableString(label)
|
||||
|
||||
val termsMatcher = Pattern.compile(terms).matcher(label)
|
||||
if (termsMatcher.find()) {
|
||||
val clickableSpan: ClickableSpan = object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.assistant_general_terms_link))
|
||||
)
|
||||
try {
|
||||
startActivity(browserIntent)
|
||||
} catch (e: Exception) {
|
||||
Log.e("[Welcome] Can't start activity: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
spannable.setSpan(
|
||||
clickableSpan,
|
||||
termsMatcher.start(0),
|
||||
termsMatcher.end(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
val policyMatcher = Pattern.compile(privacy).matcher(label)
|
||||
if (policyMatcher.find()) {
|
||||
val clickableSpan: ClickableSpan = object : ClickableSpan() {
|
||||
override fun onClick(widget: View) {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.assistant_privacy_policy_link))
|
||||
)
|
||||
try {
|
||||
startActivity(browserIntent)
|
||||
} catch (e: Exception) {
|
||||
Log.e("[Welcome] Can't start activity: $e")
|
||||
}
|
||||
}
|
||||
}
|
||||
spannable.setSpan(
|
||||
clickableSpan,
|
||||
policyMatcher.start(0),
|
||||
policyMatcher.end(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
binding.termsAndPrivacy.text = spannable
|
||||
binding.termsAndPrivacy.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +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.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.activities.assistant.fragments.CountryPickerFragment
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.DialPlan
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.PhoneNumberUtils
|
||||
|
||||
abstract class AbstractPhoneViewModel(accountCreator: AccountCreator) :
|
||||
AbstractPushTokenViewModel(accountCreator),
|
||||
CountryPickerFragment.CountryPickedListener {
|
||||
|
||||
val prefix = MutableLiveData<String>()
|
||||
val prefixError = MutableLiveData<String>()
|
||||
|
||||
val phoneNumber = MutableLiveData<String>()
|
||||
val phoneNumberError = MutableLiveData<String>()
|
||||
|
||||
val countryName = MutableLiveData<String>()
|
||||
|
||||
init {
|
||||
prefix.value = "+"
|
||||
}
|
||||
|
||||
override fun onCountryClicked(dialPlan: DialPlan) {
|
||||
prefix.value = "+${dialPlan.countryCallingCode}"
|
||||
countryName.value = dialPlan.country
|
||||
}
|
||||
|
||||
fun isPhoneNumberOk(): Boolean {
|
||||
return prefix.value.orEmpty().length > 1 && // Not just '+' character
|
||||
prefixError.value.orEmpty().isEmpty() &&
|
||||
phoneNumber.value.orEmpty().isNotEmpty() &&
|
||||
phoneNumberError.value.orEmpty().isEmpty()
|
||||
}
|
||||
|
||||
fun updateFromPhoneNumberAndOrDialPlan(number: String?, dialPlan: DialPlan?) {
|
||||
val internationalPrefix = "+${dialPlan?.countryCallingCode}"
|
||||
if (dialPlan != null) {
|
||||
Log.i("[Assistant] Found prefix from dial plan: ${dialPlan.countryCallingCode}")
|
||||
prefix.value = internationalPrefix
|
||||
getCountryNameFromPrefix(internationalPrefix)
|
||||
}
|
||||
|
||||
if (number != null) {
|
||||
Log.i("[Assistant] Found phone number: $number")
|
||||
phoneNumber.value = if (number.startsWith(internationalPrefix)) {
|
||||
number.substring(internationalPrefix.length)
|
||||
} else {
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getCountryNameFromPrefix(prefix: String?) {
|
||||
if (!prefix.isNullOrEmpty()) {
|
||||
val countryCode = if (prefix.first() == '+') prefix.substring(1) else prefix
|
||||
val dialPlan = PhoneNumberUtils.getDialPlanFromCountryCallingPrefix(countryCode)
|
||||
Log.i("[Assistant] Found dial plan $dialPlan from country code: $countryCode")
|
||||
countryName.value = dialPlan?.country
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2023 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.viewmodels
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
abstract class AbstractPushTokenViewModel(val accountCreator: AccountCreator) : ViewModel() {
|
||||
private var waitingForPushToken = false
|
||||
private var waitForPushJob: Job? = null
|
||||
|
||||
private val coreListener = object : CoreListenerStub() {
|
||||
override fun onPushNotificationReceived(core: Core, payload: String?) {
|
||||
Log.i("[Assistant] Push received: [$payload]")
|
||||
|
||||
val data = payload.orEmpty()
|
||||
if (data.isNotEmpty()) {
|
||||
try {
|
||||
// This is because JSONObject.toString() done by the SDK will result in payload looking like {"custom-payload":"{\"token\":\"value\"}"}
|
||||
val cleanPayload = data.replace("\\\"", "\"").replace("\"{", "{").replace(
|
||||
"}\"",
|
||||
"}"
|
||||
)
|
||||
Log.i("[Assistant] Cleaned payload is: [$cleanPayload]")
|
||||
val json = JSONObject(cleanPayload)
|
||||
val customPayload = json.getJSONObject("custom-payload")
|
||||
if (customPayload.has("token")) {
|
||||
waitForPushJob?.cancel()
|
||||
waitingForPushToken = false
|
||||
|
||||
val token = customPayload.getString("token")
|
||||
if (token.isNotEmpty()) {
|
||||
Log.i("[Assistant] Extracted token [$token] from push payload")
|
||||
accountCreator.token = token
|
||||
onFlexiApiTokenReceived()
|
||||
} else {
|
||||
Log.e("[Assistant] Push payload JSON object has an empty 'token'!")
|
||||
onFlexiApiTokenRequestError()
|
||||
}
|
||||
} else {
|
||||
Log.e("[Assistant] Push payload JSON object has no 'token' key!")
|
||||
onFlexiApiTokenRequestError()
|
||||
}
|
||||
} catch (e: JSONException) {
|
||||
Log.e("[Assistant] Exception trying to parse push payload as JSON: [$e]")
|
||||
onFlexiApiTokenRequestError()
|
||||
}
|
||||
} else {
|
||||
Log.e("[Assistant] Push payload is null or empty, can't extract auth token!")
|
||||
onFlexiApiTokenRequestError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(coreListener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(coreListener)
|
||||
waitForPushJob?.cancel()
|
||||
}
|
||||
|
||||
abstract fun onFlexiApiTokenReceived()
|
||||
abstract fun onFlexiApiTokenRequestError()
|
||||
|
||||
protected fun requestFlexiApiToken() {
|
||||
if (!coreContext.core.isPushNotificationAvailable) {
|
||||
Log.e(
|
||||
"[Assistant] Core says push notification aren't available, can't request a token from FlexiAPI"
|
||||
)
|
||||
onFlexiApiTokenRequestError()
|
||||
return
|
||||
}
|
||||
|
||||
val pushConfig = coreContext.core.pushNotificationConfig
|
||||
if (pushConfig != null) {
|
||||
Log.i(
|
||||
"[Assistant] Found push notification info: provider [${pushConfig.provider}], param [${pushConfig.param}] and prid [${pushConfig.prid}]"
|
||||
)
|
||||
accountCreator.pnProvider = pushConfig.provider
|
||||
accountCreator.pnParam = pushConfig.param
|
||||
accountCreator.pnPrid = pushConfig.prid
|
||||
|
||||
// Request an auth token, will be sent by push
|
||||
val result = accountCreator.requestAuthToken()
|
||||
if (result == AccountCreator.Status.RequestOk) {
|
||||
val waitFor = 5000
|
||||
waitingForPushToken = true
|
||||
waitForPushJob?.cancel()
|
||||
|
||||
Log.i("[Assistant] Waiting push with auth token for $waitFor ms")
|
||||
waitForPushJob = viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
delay(waitFor.toLong())
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
if (waitingForPushToken) {
|
||||
waitingForPushToken = false
|
||||
Log.e("[Assistant] Auth token wasn't received by push in $waitFor ms")
|
||||
onFlexiApiTokenRequestError()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e("[Assistant] Failed to require a push with an auth token: [$result]")
|
||||
onFlexiApiTokenRequestError()
|
||||
}
|
||||
} else {
|
||||
Log.e("[Assistant] No push configuration object in Core, shouldn't happen!")
|
||||
onFlexiApiTokenRequestError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,303 +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.assistant.viewmodels
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.lifecycle.*
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.PhoneNumberUtils
|
||||
|
||||
class AccountLoginViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return AccountLoginViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(accountCreator) {
|
||||
val loginWithUsernamePassword = MutableLiveData<Boolean>()
|
||||
|
||||
val username = MutableLiveData<String>()
|
||||
val usernameError = MutableLiveData<String>()
|
||||
|
||||
val password = MutableLiveData<String>()
|
||||
val passwordError = MutableLiveData<String>()
|
||||
|
||||
val loginEnabled = MediatorLiveData<Boolean>()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val displayName = MutableLiveData<String>()
|
||||
|
||||
val forceLoginUsingUsernameAndPassword = MutableLiveData<Boolean>()
|
||||
|
||||
val leaveAssistantEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val invalidCredentialsEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val goToSmsValidationEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onRecoverAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Account Login] Recover account status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
if (status == AccountCreator.Status.RequestOk) {
|
||||
goToSmsValidationEvent.value = Event(true)
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var accountToCheck: Account? = null
|
||||
|
||||
private val coreListener = object : CoreListenerStub() {
|
||||
override fun onAccountRegistrationStateChanged(
|
||||
core: Core,
|
||||
account: Account,
|
||||
state: RegistrationState?,
|
||||
message: String
|
||||
) {
|
||||
if (account == accountToCheck) {
|
||||
Log.i("[Assistant] [Account Login] Registration state is $state: $message")
|
||||
if (state == RegistrationState.Ok) {
|
||||
waitForServerAnswer.value = false
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
core.removeListener(this)
|
||||
} else if (state == RegistrationState.Failed) {
|
||||
waitForServerAnswer.value = false
|
||||
invalidCredentialsEvent.value = Event(true)
|
||||
core.removeListener(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
|
||||
val pushAvailable = LinphoneUtils.isPushNotificationAvailable()
|
||||
val deviceHasTelephonyFeature = coreContext.context.packageManager.hasSystemFeature(
|
||||
PackageManager.FEATURE_TELEPHONY
|
||||
)
|
||||
loginWithUsernamePassword.value = !deviceHasTelephonyFeature || !pushAvailable
|
||||
forceLoginUsingUsernameAndPassword.value = !pushAvailable
|
||||
|
||||
loginEnabled.value = false
|
||||
loginEnabled.addSource(prefix) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(phoneNumber) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(username) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(password) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(loginWithUsernamePassword) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(phoneNumberError) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(prefixError) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
override fun onFlexiApiTokenReceived() {
|
||||
Log.i("[Assistant] [Account Login] Using FlexiAPI auth token [${accountCreator.token}]")
|
||||
waitForServerAnswer.value = false
|
||||
loginWithPhoneNumber()
|
||||
}
|
||||
|
||||
override fun onFlexiApiTokenRequestError() {
|
||||
Log.e("[Assistant] [Account Login] Failed to get an auth token from FlexiAPI")
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: Failed to get an auth token from account manager server")
|
||||
}
|
||||
|
||||
fun removeInvalidProxyConfig() {
|
||||
val account = accountToCheck
|
||||
account ?: return
|
||||
|
||||
val core = coreContext.core
|
||||
val authInfo = account.findAuthInfo()
|
||||
if (authInfo != null) core.removeAuthInfo(authInfo)
|
||||
core.removeAccount(account)
|
||||
accountToCheck = null
|
||||
|
||||
// Make sure there is a valid default account
|
||||
val accounts = core.accountList
|
||||
if (accounts.isNotEmpty() && core.defaultAccount == null) {
|
||||
core.defaultAccount = accounts.first()
|
||||
core.refreshRegisters()
|
||||
}
|
||||
}
|
||||
|
||||
fun continueEvenIfInvalidCredentials() {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
}
|
||||
|
||||
private fun loginWithUsername() {
|
||||
val result = accountCreator.setUsername(username.value)
|
||||
if (result != AccountCreator.UsernameStatus.Ok) {
|
||||
Log.e(
|
||||
"[Assistant] [Account Login] Error [${result.name}] setting the username: ${username.value}"
|
||||
)
|
||||
usernameError.value = result.name
|
||||
return
|
||||
}
|
||||
Log.i("[Assistant] [Account Login] Username is ${accountCreator.username}")
|
||||
|
||||
val result2 = accountCreator.setPassword(password.value)
|
||||
if (result2 != AccountCreator.PasswordStatus.Ok) {
|
||||
Log.e("[Assistant] [Account Login] Error [${result2.name}] setting the password")
|
||||
passwordError.value = result2.name
|
||||
return
|
||||
}
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
coreContext.core.addListener(coreListener)
|
||||
if (!createAccountAndAuthInfo()) {
|
||||
waitForServerAnswer.value = false
|
||||
coreContext.core.removeListener(coreListener)
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
}
|
||||
}
|
||||
|
||||
private fun loginWithPhoneNumber() {
|
||||
val result = AccountCreator.PhoneNumberStatus.fromInt(
|
||||
accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
|
||||
)
|
||||
if (result != AccountCreator.PhoneNumberStatus.Ok) {
|
||||
Log.e(
|
||||
"[Assistant] [Account Login] Error [$result] setting the phone number: ${phoneNumber.value} with prefix: ${prefix.value}"
|
||||
)
|
||||
phoneNumberError.value = result.name
|
||||
return
|
||||
}
|
||||
Log.i("[Assistant] [Account Login] Phone number is ${accountCreator.phoneNumber}")
|
||||
|
||||
val result2 = accountCreator.setUsername(accountCreator.phoneNumber)
|
||||
if (result2 != AccountCreator.UsernameStatus.Ok) {
|
||||
Log.e(
|
||||
"[Assistant] [Account Login] Error [${result2.name}] setting the username: ${accountCreator.phoneNumber}"
|
||||
)
|
||||
usernameError.value = result2.name
|
||||
return
|
||||
}
|
||||
Log.i("[Assistant] [Account Login] Username is ${accountCreator.username}")
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
val status = accountCreator.recoverAccount()
|
||||
Log.i("[Assistant] [Account Login] Recover account returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
fun login() {
|
||||
accountCreator.displayName = displayName.value
|
||||
|
||||
if (loginWithUsernamePassword.value == true) {
|
||||
loginWithUsername()
|
||||
} else {
|
||||
val token = accountCreator.token.orEmpty()
|
||||
if (token.isNotEmpty()) {
|
||||
Log.i(
|
||||
"[Assistant] [Account Login] We already have an auth token from FlexiAPI [$token], continue"
|
||||
)
|
||||
onFlexiApiTokenReceived()
|
||||
} else {
|
||||
Log.i("[Assistant] [Account Login] Requesting an auth token from FlexiAPI")
|
||||
waitForServerAnswer.value = true
|
||||
requestFlexiApiToken()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isLoginButtonEnabled(): Boolean {
|
||||
return if (loginWithUsernamePassword.value == true) {
|
||||
username.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty()
|
||||
} else {
|
||||
isPhoneNumberOk()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAccountAndAuthInfo(): Boolean {
|
||||
val account = accountCreator.createAccountInCore()
|
||||
accountToCheck = account
|
||||
|
||||
if (account == null) {
|
||||
Log.e("[Assistant] [Account Login] Account creator couldn't create account")
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
return false
|
||||
}
|
||||
|
||||
val params = account.params.clone()
|
||||
params.pushNotificationAllowed = true
|
||||
|
||||
if (params.internationalPrefix.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}"
|
||||
)
|
||||
params.internationalPrefix = dialPlan.countryCallingCode
|
||||
} else {
|
||||
Log.w("[Assistant] [Account Login] Failed to find dial plan")
|
||||
}
|
||||
}
|
||||
|
||||
account.params = params
|
||||
Log.i("[Assistant] [Account Login] Account created")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +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.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.EcCalibratorStatus
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class EchoCancellerCalibrationViewModel : ViewModel() {
|
||||
val echoCalibrationTerminated = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onEcCalibrationResult(core: Core, status: EcCalibratorStatus, delayMs: Int) {
|
||||
if (status == EcCalibratorStatus.InProgress) return
|
||||
echoCancellerCalibrationFinished(status, delayMs)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
}
|
||||
|
||||
fun startEchoCancellerCalibration() {
|
||||
coreContext.core.startEchoCancellerCalibration()
|
||||
}
|
||||
|
||||
fun echoCancellerCalibrationFinished(status: EcCalibratorStatus, delay: Int) {
|
||||
coreContext.core.removeListener(listener)
|
||||
when (status) {
|
||||
EcCalibratorStatus.DoneNoEcho -> {
|
||||
Log.i("[Assistant] [Echo Canceller Calibration] Done, no echo")
|
||||
}
|
||||
EcCalibratorStatus.Done -> {
|
||||
Log.i("[Assistant] [Echo Canceller Calibration] Done, delay is ${delay}ms")
|
||||
}
|
||||
EcCalibratorStatus.Failed -> {
|
||||
Log.w("[Assistant] [Echo Canceller Calibration] Failed")
|
||||
}
|
||||
EcCalibratorStatus.InProgress -> {
|
||||
Log.i("[Assistant] [Echo Canceller Calibration] In progress")
|
||||
}
|
||||
}
|
||||
echoCalibrationTerminated.value = Event(true)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,199 +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.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class EmailAccountCreationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return EmailAccountCreationViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class EmailAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPushTokenViewModel(
|
||||
accountCreator
|
||||
) {
|
||||
val username = MutableLiveData<String>()
|
||||
val usernameError = MutableLiveData<String>()
|
||||
|
||||
val email = MutableLiveData<String>()
|
||||
val emailError = MutableLiveData<String>()
|
||||
|
||||
val password = MutableLiveData<String>()
|
||||
val passwordError = MutableLiveData<String>()
|
||||
|
||||
val passwordConfirmation = MutableLiveData<String>()
|
||||
val passwordConfirmationError = MutableLiveData<String>()
|
||||
|
||||
val displayName = MutableLiveData<String>()
|
||||
|
||||
val createEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val goToEmailValidationEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onIsAccountExist(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Account Creation] onIsAccountExist status is $status")
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> {
|
||||
waitForServerAnswer.value = false
|
||||
usernameError.value = AppUtils.getString(
|
||||
R.string.assistant_error_username_already_exists
|
||||
)
|
||||
}
|
||||
AccountCreator.Status.AccountNotExist -> {
|
||||
val createAccountStatus = creator.createAccount()
|
||||
if (createAccountStatus != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Account Creation] onCreateAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountCreated -> {
|
||||
goToEmailValidationEvent.value = Event(true)
|
||||
}
|
||||
else -> {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
|
||||
createEnabled.value = false
|
||||
createEnabled.addSource(username) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(usernameError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(email) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(emailError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(password) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(passwordError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(passwordConfirmation) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(passwordConfirmationError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
override fun onFlexiApiTokenReceived() {
|
||||
Log.i("[Assistant] [Account Creation] Using FlexiAPI auth token [${accountCreator.token}]")
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
val status = accountCreator.isAccountExist
|
||||
Log.i("[Assistant] [Account Creation] Account exists returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFlexiApiTokenRequestError() {
|
||||
Log.e("[Assistant] [Account Creation] Failed to get an auth token from FlexiAPI")
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: Failed to get an auth token from account manager server")
|
||||
}
|
||||
|
||||
fun create() {
|
||||
accountCreator.username = username.value
|
||||
accountCreator.password = password.value
|
||||
accountCreator.email = email.value
|
||||
accountCreator.displayName = displayName.value
|
||||
|
||||
val token = accountCreator.token.orEmpty()
|
||||
if (token.isNotEmpty()) {
|
||||
Log.i(
|
||||
"[Assistant] [Account Creation] We already have an auth token from FlexiAPI [$token], continue"
|
||||
)
|
||||
onFlexiApiTokenReceived()
|
||||
} else {
|
||||
Log.i("[Assistant] [Account Creation] Requesting an auth token from FlexiAPI")
|
||||
waitForServerAnswer.value = true
|
||||
requestFlexiApiToken()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCreateButtonEnabled(): Boolean {
|
||||
return username.value.orEmpty().isNotEmpty() &&
|
||||
email.value.orEmpty().isNotEmpty() &&
|
||||
password.value.orEmpty().isNotEmpty() &&
|
||||
passwordConfirmation.value.orEmpty().isNotEmpty() &&
|
||||
password.value == passwordConfirmation.value &&
|
||||
usernameError.value.orEmpty().isEmpty() &&
|
||||
emailError.value.orEmpty().isEmpty() &&
|
||||
passwordError.value.orEmpty().isEmpty() &&
|
||||
passwordConfirmationError.value.orEmpty().isEmpty()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,132 +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.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.PhoneNumberUtils
|
||||
|
||||
class EmailAccountValidationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return EmailAccountValidationViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class EmailAccountValidationViewModel(val accountCreator: AccountCreator) : ViewModel() {
|
||||
val email = MutableLiveData<String>()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onIsAccountActivated(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Account Validation] onIsAccountActivated status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountActivated -> {
|
||||
if (createAccountAndAuthInfo()) {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
AccountCreator.Status.AccountNotActivated -> {
|
||||
onErrorEvent.value = Event(
|
||||
AppUtils.getString(R.string.assistant_create_email_account_not_validated)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
email.value = accountCreator.email
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun finish() {
|
||||
waitForServerAnswer.value = true
|
||||
val status = accountCreator.isAccountActivated
|
||||
Log.i("[Assistant] [Account Validation] Account exists returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAccountAndAuthInfo(): Boolean {
|
||||
val account = accountCreator.createAccountInCore()
|
||||
|
||||
if (account == null) {
|
||||
Log.e("[Assistant] [Account Validation] Account creator couldn't create account")
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
return false
|
||||
}
|
||||
|
||||
val params = account.params.clone()
|
||||
params.pushNotificationAllowed = true
|
||||
|
||||
if (params.internationalPrefix.isNullOrEmpty()) {
|
||||
val dialPlan = PhoneNumberUtils.getDialPlanForCurrentCountry(coreContext.context)
|
||||
if (dialPlan != null) {
|
||||
Log.i(
|
||||
"[Assistant] [Account Validation] Found dial plan country ${dialPlan.country} with international prefix ${dialPlan.countryCallingCode}"
|
||||
)
|
||||
params.internationalPrefix = dialPlan.countryCallingCode
|
||||
} else {
|
||||
Log.w("[Assistant] [Account Validation] Failed to find dial plan")
|
||||
}
|
||||
}
|
||||
|
||||
account.params = params
|
||||
|
||||
Log.i("[Assistant] [Account Validation] Account created")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,159 +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.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class GenericLoginViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return GenericLoginViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewModel() {
|
||||
val username = MutableLiveData<String>()
|
||||
|
||||
val password = MutableLiveData<String>()
|
||||
|
||||
val domain = MutableLiveData<String>()
|
||||
|
||||
val displayName = MutableLiveData<String>()
|
||||
|
||||
val transport = MutableLiveData<TransportType>()
|
||||
|
||||
val loginEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val invalidCredentialsEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private var accountToCheck: Account? = null
|
||||
|
||||
private val coreListener = object : CoreListenerStub() {
|
||||
override fun onAccountRegistrationStateChanged(
|
||||
core: Core,
|
||||
account: Account,
|
||||
state: RegistrationState?,
|
||||
message: String
|
||||
) {
|
||||
if (account == accountToCheck) {
|
||||
Log.i("[Assistant] [Generic Login] Registration state is $state: $message")
|
||||
if (state == RegistrationState.Ok) {
|
||||
waitForServerAnswer.value = false
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
core.removeListener(this)
|
||||
} else if (state == RegistrationState.Failed) {
|
||||
waitForServerAnswer.value = false
|
||||
invalidCredentialsEvent.value = Event(true)
|
||||
core.removeListener(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
transport.value = TransportType.Tls
|
||||
|
||||
loginEnabled.value = false
|
||||
loginEnabled.addSource(username) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(password) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(domain) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
fun setTransport(transportType: TransportType) {
|
||||
transport.value = transportType
|
||||
}
|
||||
|
||||
fun removeInvalidProxyConfig() {
|
||||
val account = accountToCheck
|
||||
account ?: return
|
||||
|
||||
val core = coreContext.core
|
||||
val authInfo = account.findAuthInfo()
|
||||
if (authInfo != null) core.removeAuthInfo(authInfo)
|
||||
core.removeAccount(account)
|
||||
accountToCheck = null
|
||||
|
||||
// Make sure there is a valid default account
|
||||
val accounts = core.accountList
|
||||
if (accounts.isNotEmpty() && core.defaultAccount == null) {
|
||||
core.defaultAccount = accounts.first()
|
||||
core.refreshRegisters()
|
||||
}
|
||||
}
|
||||
|
||||
fun continueEvenIfInvalidCredentials() {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
}
|
||||
|
||||
fun createAccountAndAuthInfo() {
|
||||
waitForServerAnswer.value = true
|
||||
coreContext.core.addListener(coreListener)
|
||||
|
||||
accountCreator.username = username.value
|
||||
accountCreator.password = password.value
|
||||
accountCreator.domain = domain.value
|
||||
accountCreator.displayName = displayName.value
|
||||
accountCreator.transport = transport.value
|
||||
|
||||
val account = accountCreator.createAccountInCore()
|
||||
accountToCheck = account
|
||||
|
||||
if (account == null) {
|
||||
Log.e("[Assistant] [Generic Login] Account creator couldn't create account")
|
||||
coreContext.core.removeListener(coreListener)
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
waitForServerAnswer.value = false
|
||||
return
|
||||
}
|
||||
|
||||
Log.i("[Assistant] [Generic Login] Account created")
|
||||
}
|
||||
|
||||
private fun isLoginButtonEnabled(): Boolean {
|
||||
return username.value.orEmpty().isNotEmpty() &&
|
||||
domain.value.orEmpty().isNotEmpty() &&
|
||||
password.value.orEmpty().isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,271 +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.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class PhoneAccountCreationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return PhoneAccountCreationViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(
|
||||
accountCreator
|
||||
) {
|
||||
val username = MutableLiveData<String>()
|
||||
val useUsername = MutableLiveData<Boolean>()
|
||||
val usernameError = MutableLiveData<String>()
|
||||
|
||||
val displayName = MutableLiveData<String>()
|
||||
|
||||
val createEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val goToSmsValidationEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onIsAccountExist(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Phone Account Creation] onIsAccountExist status is $status")
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> {
|
||||
waitForServerAnswer.value = false
|
||||
usernameError.value = AppUtils.getString(
|
||||
R.string.assistant_error_username_already_exists
|
||||
)
|
||||
}
|
||||
AccountCreator.Status.AccountNotExist -> {
|
||||
waitForServerAnswer.value = false
|
||||
checkPhoneNumber()
|
||||
}
|
||||
else -> {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIsAliasUsed(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Phone Account Creation] onIsAliasUsed status is $status")
|
||||
when (status) {
|
||||
AccountCreator.Status.AliasExist -> {
|
||||
waitForServerAnswer.value = false
|
||||
phoneNumberError.value = AppUtils.getString(
|
||||
R.string.assistant_error_phone_number_already_exists
|
||||
)
|
||||
}
|
||||
AccountCreator.Status.AliasIsAccount -> {
|
||||
waitForServerAnswer.value = false
|
||||
if (useUsername.value == true) {
|
||||
usernameError.value = AppUtils.getString(
|
||||
R.string.assistant_error_username_already_exists
|
||||
)
|
||||
} else {
|
||||
phoneNumberError.value = AppUtils.getString(
|
||||
R.string.assistant_error_phone_number_already_exists
|
||||
)
|
||||
}
|
||||
}
|
||||
AccountCreator.Status.AliasNotExist -> {
|
||||
val createAccountStatus = creator.createAccount()
|
||||
Log.i(
|
||||
"[Assistant] [Phone Account Creation] createAccount returned $createAccountStatus"
|
||||
)
|
||||
if (createAccountStatus != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Phone Account Creation] onCreateAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountCreated -> {
|
||||
goToSmsValidationEvent.value = Event(true)
|
||||
}
|
||||
AccountCreator.Status.AccountExistWithAlias -> {
|
||||
phoneNumberError.value = AppUtils.getString(
|
||||
R.string.assistant_error_phone_number_already_exists
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
useUsername.value = false
|
||||
accountCreator.addListener(listener)
|
||||
|
||||
createEnabled.value = false
|
||||
createEnabled.addSource(prefix) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(phoneNumber) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(useUsername) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(username) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(usernameError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(phoneNumberError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(prefixError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
override fun onFlexiApiTokenReceived() {
|
||||
Log.i(
|
||||
"[Assistant] [Phone Account Creation] Using FlexiAPI auth token [${accountCreator.token}]"
|
||||
)
|
||||
accountCreator.displayName = displayName.value
|
||||
|
||||
val result = AccountCreator.PhoneNumberStatus.fromInt(
|
||||
accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
|
||||
)
|
||||
if (result != AccountCreator.PhoneNumberStatus.Ok) {
|
||||
Log.e(
|
||||
"[Assistant] [Phone Account Creation] Error [$result] setting the phone number: ${phoneNumber.value} with prefix: ${prefix.value}"
|
||||
)
|
||||
phoneNumberError.value = result.name
|
||||
return
|
||||
}
|
||||
Log.i("[Assistant] [Phone Account Creation] Phone number is ${accountCreator.phoneNumber}")
|
||||
|
||||
if (useUsername.value == true) {
|
||||
accountCreator.username = username.value
|
||||
} else {
|
||||
accountCreator.username = accountCreator.phoneNumber
|
||||
}
|
||||
|
||||
if (useUsername.value == true) {
|
||||
checkUsername()
|
||||
} else {
|
||||
checkPhoneNumber()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFlexiApiTokenRequestError() {
|
||||
Log.e("[Assistant] [Phone Account Creation] Failed to get an auth token from FlexiAPI")
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: Failed to get an auth token from account manager server")
|
||||
}
|
||||
|
||||
private fun checkUsername() {
|
||||
val status = accountCreator.isAccountExist
|
||||
Log.i("[Assistant] [Phone Account Creation] isAccountExist returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPhoneNumber() {
|
||||
val status = accountCreator.isAliasUsed
|
||||
Log.i("[Assistant] [Phone Account Creation] isAliasUsed returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
fun create() {
|
||||
val token = accountCreator.token.orEmpty()
|
||||
if (token.isNotEmpty()) {
|
||||
Log.i(
|
||||
"[Assistant] [Phone Account Creation] We already have an auth token from FlexiAPI [$token], continue"
|
||||
)
|
||||
onFlexiApiTokenReceived()
|
||||
} else {
|
||||
Log.i("[Assistant] [Phone Account Creation] Requesting an auth token from FlexiAPI")
|
||||
waitForServerAnswer.value = true
|
||||
requestFlexiApiToken()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCreateButtonEnabled(): Boolean {
|
||||
val usernameRegexp = corePreferences.config.getString(
|
||||
"assistant",
|
||||
"username_regex",
|
||||
"^[a-z0-9+_.\\-]*\$"
|
||||
)
|
||||
return isPhoneNumberOk() && usernameRegexp != null &&
|
||||
(
|
||||
useUsername.value == false ||
|
||||
username.value.orEmpty().matches(Regex(usernameRegexp)) &&
|
||||
username.value.orEmpty().isNotEmpty() &&
|
||||
usernameError.value.orEmpty().isEmpty()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,157 +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.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class PhoneAccountLinkingViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return PhoneAccountLinkingViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(
|
||||
accountCreator
|
||||
) {
|
||||
val username = MutableLiveData<String>()
|
||||
|
||||
val allowSkip = MutableLiveData<Boolean>()
|
||||
|
||||
val linkEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val goToSmsValidationEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onIsAliasUsed(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Phone Account Linking] onIsAliasUsed status is $status")
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.AliasNotExist -> {
|
||||
if (creator.linkAccount() != AccountCreator.Status.RequestOk) {
|
||||
Log.e("[Assistant] [Phone Account Linking] linkAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
AccountCreator.Status.AliasExist, AccountCreator.Status.AliasIsAccount -> {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
else -> {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLinkAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Phone Account Linking] onLinkAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.RequestOk -> {
|
||||
goToSmsValidationEvent.value = Event(true)
|
||||
}
|
||||
else -> {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
|
||||
linkEnabled.value = false
|
||||
linkEnabled.addSource(prefix) {
|
||||
linkEnabled.value = isLinkButtonEnabled()
|
||||
}
|
||||
linkEnabled.addSource(phoneNumber) {
|
||||
linkEnabled.value = isLinkButtonEnabled()
|
||||
}
|
||||
linkEnabled.addSource(phoneNumberError) {
|
||||
linkEnabled.value = isLinkButtonEnabled()
|
||||
}
|
||||
linkEnabled.addSource(prefixError) {
|
||||
linkEnabled.value = isLinkButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
override fun onFlexiApiTokenReceived() {
|
||||
accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
|
||||
accountCreator.username = username.value
|
||||
Log.i("[Assistant] [Phone Account Linking] Phone number is ${accountCreator.phoneNumber}")
|
||||
|
||||
val status: AccountCreator.Status = accountCreator.isAliasUsed
|
||||
Log.i("[Assistant] [Phone Account Linking] isAliasUsed returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFlexiApiTokenRequestError() {
|
||||
Log.e("[Assistant] [Phone Account Linking] Failed to get an auth token from FlexiAPI")
|
||||
waitForServerAnswer.value = false
|
||||
}
|
||||
|
||||
fun link() {
|
||||
Log.i("[Assistant] [Phone Account Linking] Requesting an auth token from FlexiAPI")
|
||||
waitForServerAnswer.value = true
|
||||
requestFlexiApiToken()
|
||||
}
|
||||
|
||||
fun skip() {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
}
|
||||
|
||||
private fun isLinkButtonEnabled(): Boolean {
|
||||
return isPhoneNumberOk()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,162 +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.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class PhoneAccountValidationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return PhoneAccountValidationViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneAccountValidationViewModel(val accountCreator: AccountCreator) : ViewModel() {
|
||||
val phoneNumber = MutableLiveData<String>()
|
||||
|
||||
val code = MutableLiveData<String>()
|
||||
|
||||
val isLogin = MutableLiveData<Boolean>()
|
||||
|
||||
val isCreation = MutableLiveData<Boolean>()
|
||||
|
||||
val isLinking = MutableLiveData<Boolean>()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val onErrorEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onLoginLinphoneAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Phone Account Validation] onLoginLinphoneAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
if (status == AccountCreator.Status.RequestOk) {
|
||||
if (createAccountAndAuthInfo()) {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
}
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivateAlias(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Phone Account Validation] onActivateAlias status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountActivated -> {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
}
|
||||
else -> {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivateAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Phone Account Validation] onActivateAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
if (status == AccountCreator.Status.AccountActivated) {
|
||||
if (createAccountAndAuthInfo()) {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: Failed to create account object")
|
||||
}
|
||||
} else {
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun finish() {
|
||||
accountCreator.activationCode = code.value.orEmpty()
|
||||
Log.i(
|
||||
"[Assistant] [Phone Account Validation] Phone number is ${accountCreator.phoneNumber} and activation code is ${accountCreator.activationCode}"
|
||||
)
|
||||
waitForServerAnswer.value = true
|
||||
|
||||
val status = when {
|
||||
isLogin.value == true -> accountCreator.loginLinphoneAccount()
|
||||
isCreation.value == true -> accountCreator.activateAccount()
|
||||
isLinking.value == true -> accountCreator.activateAlias()
|
||||
else -> AccountCreator.Status.UnexpectedError
|
||||
}
|
||||
Log.i("[Assistant] [Phone Account Validation] Code validation result is $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
onErrorEvent.value = Event("Error: ${status.name}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAccountAndAuthInfo(): Boolean {
|
||||
val account = accountCreator.createAccountInCore()
|
||||
|
||||
if (account == null) {
|
||||
Log.e(
|
||||
"[Assistant] [Phone Account Validation] Account creator couldn't create account"
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
val params = account.params.clone()
|
||||
params.pushNotificationAllowed = true
|
||||
account.params = params
|
||||
|
||||
Log.i("[Assistant] [Phone Account Validation] Account created")
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +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.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class QrCodeViewModel : ViewModel() {
|
||||
val qrCodeFoundEvent = MutableLiveData<Event<String>>()
|
||||
|
||||
val showSwitchCamera = MutableLiveData<Boolean>()
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onQrcodeFound(core: Core, result: String?) {
|
||||
Log.i("[Assistant] [QR Code] Found [$result]")
|
||||
if (result != null) qrCodeFoundEvent.postValue(Event(result))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
showSwitchCamera.value = coreContext.showSwitchCameraButton()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun setBackCamera() {
|
||||
showSwitchCamera.value = coreContext.showSwitchCameraButton()
|
||||
|
||||
for (camera in coreContext.core.videoDevicesList) {
|
||||
if (camera.contains("Back")) {
|
||||
Log.i("[Assistant] [QR Code] Found back facing camera: $camera")
|
||||
coreContext.core.videoDevice = camera
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val first = coreContext.core.videoDevicesList.firstOrNull()
|
||||
if (first != null) {
|
||||
Log.i("[Assistant] [QR Code] Using first camera found: $first")
|
||||
coreContext.core.videoDevice = first
|
||||
}
|
||||
}
|
||||
|
||||
fun switchCamera() {
|
||||
coreContext.switchCamera()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,89 +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.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.ConfiguringState
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class RemoteProvisioningViewModel : ViewModel() {
|
||||
val urlToFetch = MutableLiveData<String>()
|
||||
val urlError = MutableLiveData<String>()
|
||||
|
||||
val fetchEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
val fetchInProgress = MutableLiveData<Boolean>()
|
||||
val fetchSuccessfulEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onConfiguringStatus(
|
||||
core: Core,
|
||||
status: ConfiguringState,
|
||||
message: String?
|
||||
) {
|
||||
fetchInProgress.value = false
|
||||
when (status) {
|
||||
ConfiguringState.Successful -> {
|
||||
fetchSuccessfulEvent.value = Event(true)
|
||||
}
|
||||
ConfiguringState.Failed -> {
|
||||
fetchSuccessfulEvent.value = Event(false)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
fetchInProgress.value = false
|
||||
coreContext.core.addListener(listener)
|
||||
|
||||
fetchEnabled.value = false
|
||||
fetchEnabled.addSource(urlToFetch) {
|
||||
fetchEnabled.value = isFetchEnabled()
|
||||
}
|
||||
fetchEnabled.addSource(urlError) {
|
||||
fetchEnabled.value = isFetchEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun fetchAndApply() {
|
||||
val url = urlToFetch.value.orEmpty()
|
||||
coreContext.core.provisioningUri = url
|
||||
Log.w("[Assistant] [Remote Provisioning] Url set to [$url], restarting Core")
|
||||
fetchInProgress.value = true
|
||||
coreContext.core.stop()
|
||||
coreContext.core.start()
|
||||
}
|
||||
|
||||
private fun isFetchEnabled(): Boolean {
|
||||
return urlToFetch.value.orEmpty().isNotEmpty() && urlError.value.orEmpty().isEmpty()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +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.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import java.util.*
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class SharedAssistantViewModel : ViewModel() {
|
||||
val remoteProvisioningUrl = MutableLiveData<String>()
|
||||
|
||||
private var accountCreator: AccountCreator
|
||||
private var useGenericSipAccount: Boolean = false
|
||||
|
||||
init {
|
||||
Log.i("[Assistant] Loading linphone default values")
|
||||
coreContext.core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath)
|
||||
accountCreator = coreContext.core.createAccountCreator(corePreferences.xmlRpcServerUrl)
|
||||
accountCreator.language = Locale.getDefault().language
|
||||
}
|
||||
|
||||
fun getAccountCreator(genericAccountCreator: Boolean = false): AccountCreator {
|
||||
if (genericAccountCreator != useGenericSipAccount) {
|
||||
accountCreator.reset()
|
||||
accountCreator.language = Locale.getDefault().language
|
||||
|
||||
if (genericAccountCreator) {
|
||||
Log.i("[Assistant] Loading default values")
|
||||
coreContext.core.loadConfigFromXml(corePreferences.defaultValuesPath)
|
||||
} else {
|
||||
Log.i("[Assistant] Loading linphone default values")
|
||||
coreContext.core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath)
|
||||
}
|
||||
useGenericSipAccount = genericAccountCreator
|
||||
}
|
||||
return accountCreator
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +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.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
|
||||
class WelcomeViewModel : ViewModel() {
|
||||
val showCreateAccount: Boolean = corePreferences.showCreateAccount
|
||||
val showLinphoneLogin: Boolean = corePreferences.showLinphoneLogin
|
||||
val showGenericLogin: Boolean = corePreferences.showGenericLogin
|
||||
val showRemoteProvisioning: Boolean = corePreferences.showRemoteProvisioning
|
||||
|
||||
val termsAndPrivacyAccepted = MutableLiveData<Boolean>()
|
||||
|
||||
init {
|
||||
termsAndPrivacyAccepted.value = corePreferences.readAndAgreeTermsAndPrivacy
|
||||
}
|
||||
}
|
||||
|
|
@ -1,225 +0,0 @@
|
|||
/*
|
||||
* 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.chat_bubble
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericActivity
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.chat.adapters.ChatMessagesListAdapter
|
||||
import org.linphone.activities.main.chat.viewmodels.*
|
||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.ChatRoomListenerStub
|
||||
import org.linphone.core.EventLog
|
||||
import org.linphone.core.Factory
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatBubbleActivityBinding
|
||||
import org.linphone.utils.FileUtils
|
||||
|
||||
class ChatBubbleActivity : GenericActivity() {
|
||||
private lateinit var binding: ChatBubbleActivityBinding
|
||||
private lateinit var viewModel: ChatRoomViewModel
|
||||
private lateinit var listViewModel: ChatMessagesListViewModel
|
||||
private lateinit var chatSendingViewModel: ChatMessageSendingViewModel
|
||||
private lateinit var adapter: ChatMessagesListAdapter
|
||||
|
||||
private val observer = object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
if (positionStart == adapter.itemCount - itemCount) {
|
||||
adapter.notifyItemChanged(positionStart - 1) // For grouping purposes
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val listener = object : ChatRoomListenerStub() {
|
||||
override fun onChatMessagesReceived(chatRoom: ChatRoom, eventLogs: Array<out EventLog>) {
|
||||
chatRoom.markAsRead()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.chat_bubble_activity)
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
val localSipUri = intent.getStringExtra("LocalSipUri")
|
||||
val remoteSipUri = intent.getStringExtra("RemoteSipUri")
|
||||
var chatRoom: ChatRoom? = null
|
||||
|
||||
if (localSipUri != null && remoteSipUri != null) {
|
||||
Log.i(
|
||||
"[Chat Bubble] Found local [$localSipUri] & remote [$remoteSipUri] addresses in arguments"
|
||||
)
|
||||
val localAddress = Factory.instance().createAddress(localSipUri)
|
||||
val remoteSipAddress = Factory.instance().createAddress(remoteSipUri)
|
||||
chatRoom = coreContext.core.searchChatRoom(
|
||||
null,
|
||||
localAddress,
|
||||
remoteSipAddress,
|
||||
arrayOfNulls(
|
||||
0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (chatRoom == null) {
|
||||
Log.e("[Chat Bubble] Chat room is null, aborting!")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
ChatRoomViewModelFactory(chatRoom)
|
||||
)[ChatRoomViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
listViewModel = ViewModelProvider(
|
||||
this,
|
||||
ChatMessagesListViewModelFactory(chatRoom)
|
||||
)[ChatMessagesListViewModel::class.java]
|
||||
|
||||
chatSendingViewModel = ViewModelProvider(
|
||||
this,
|
||||
ChatMessageSendingViewModelFactory(chatRoom)
|
||||
)[ChatMessageSendingViewModel::class.java]
|
||||
binding.chatSendingViewModel = chatSendingViewModel
|
||||
|
||||
val listSelectionViewModel = ViewModelProvider(this)[ListTopBarViewModel::class.java]
|
||||
adapter = ChatMessagesListAdapter(listSelectionViewModel, this)
|
||||
// SubmitList is done on a background thread
|
||||
// We need this adapter data observer to know when to scroll
|
||||
binding.chatMessagesList.adapter = adapter
|
||||
adapter.registerAdapterDataObserver(observer)
|
||||
|
||||
// Disable context menu on each message
|
||||
adapter.disableAdvancedContextMenuOptions()
|
||||
|
||||
adapter.openContentEvent.observe(
|
||||
this
|
||||
) {
|
||||
it.consume { content ->
|
||||
if (content.isFileEncrypted) {
|
||||
Toast.makeText(
|
||||
this,
|
||||
R.string.chat_bubble_cant_open_enrypted_file,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
} else {
|
||||
FileUtils.openFileInThirdPartyApp(this, content.filePath.orEmpty(), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val layoutManager = LinearLayoutManager(this)
|
||||
layoutManager.stackFromEnd = true
|
||||
binding.chatMessagesList.layoutManager = layoutManager
|
||||
|
||||
listViewModel.events.observe(
|
||||
this
|
||||
) { events ->
|
||||
adapter.submitList(events)
|
||||
}
|
||||
|
||||
chatSendingViewModel.textToSend.observe(
|
||||
this
|
||||
) {
|
||||
chatSendingViewModel.onTextToSendChanged(it)
|
||||
}
|
||||
|
||||
binding.setOpenAppClickListener {
|
||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
||||
coreContext.notificationsManager.changeDismissNotificationUponReadForChatRoom(
|
||||
viewModel.chatRoom,
|
||||
false
|
||||
)
|
||||
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.putExtra("RemoteSipUri", remoteSipUri)
|
||||
intent.putExtra("LocalSipUri", localSipUri)
|
||||
intent.putExtra("Chat", true)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK and Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
binding.setCloseBubbleClickListener {
|
||||
coreContext.notificationsManager.dismissChatNotification(viewModel.chatRoom)
|
||||
}
|
||||
|
||||
binding.setSendMessageClickListener {
|
||||
chatSendingViewModel.sendMessage()
|
||||
binding.message.text?.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
viewModel.chatRoom.addListener(listener)
|
||||
|
||||
// Workaround for the removed notification when a chat room is marked as read
|
||||
coreContext.notificationsManager.changeDismissNotificationUponReadForChatRoom(
|
||||
viewModel.chatRoom,
|
||||
true
|
||||
)
|
||||
viewModel.chatRoom.markAsRead()
|
||||
|
||||
val peerAddress = viewModel.chatRoom.peerAddress.asStringUriOnly()
|
||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = peerAddress
|
||||
coreContext.notificationsManager.resetChatNotificationCounterForSipUri(peerAddress)
|
||||
|
||||
lifecycleScope.launch {
|
||||
// Without the delay the scroll to bottom doesn't happen...
|
||||
delay(100)
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
viewModel.chatRoom.removeListener(listener)
|
||||
|
||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
||||
coreContext.notificationsManager.changeDismissNotificationUponReadForChatRoom(
|
||||
viewModel.chatRoom,
|
||||
false
|
||||
)
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun scrollToBottom() {
|
||||
if (adapter.itemCount > 0) {
|
||||
binding.chatMessagesList.scrollToPosition(adapter.itemCount - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,740 +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.main
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.Gravity
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.window.layout.FoldingFeature
|
||||
import coil.imageLoader
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.net.URLDecoder
|
||||
import kotlin.math.abs
|
||||
import kotlinx.coroutines.*
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.*
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.main.viewmodels.CallOverlayViewModel
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.activities.navigateToDialer
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||
import org.linphone.core.AuthInfo
|
||||
import org.linphone.core.AuthMethod
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.CorePreferences
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.MainActivityBinding
|
||||
import org.linphone.utils.*
|
||||
|
||||
class MainActivity : GenericActivity(), SnackBarActivity, NavController.OnDestinationChangedListener {
|
||||
private lateinit var binding: MainActivityBinding
|
||||
private lateinit var sharedViewModel: SharedMainViewModel
|
||||
private lateinit var callOverlayViewModel: CallOverlayViewModel
|
||||
|
||||
private val listener = object : ContactsUpdatedListenerStub() {
|
||||
override fun onContactsUpdated() {
|
||||
Log.i("[Main Activity] Contact(s) updated, update shortcuts")
|
||||
if (corePreferences.contactsShortcuts) {
|
||||
ShortcutsHelper.createShortcutsToContacts(this@MainActivity)
|
||||
} else if (corePreferences.chatRoomShortcuts) {
|
||||
ShortcutsHelper.createShortcutsToChatRooms(this@MainActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var tabsFragment: FragmentContainerView
|
||||
private lateinit var statusFragment: FragmentContainerView
|
||||
|
||||
private var overlayX = 0f
|
||||
private var overlayY = 0f
|
||||
private var initPosX = 0f
|
||||
private var initPosY = 0f
|
||||
private var overlay: View? = null
|
||||
|
||||
private val componentCallbacks = object : ComponentCallbacks2 {
|
||||
override fun onConfigurationChanged(newConfig: Configuration) { }
|
||||
|
||||
override fun onLowMemory() {
|
||||
Log.w("[Main Activity] onLowMemory !")
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
Log.w("[Main Activity] onTrimMemory called with level $level !")
|
||||
applicationContext.imageLoader.memoryCache?.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLayoutChanges(foldingFeature: FoldingFeature?) {
|
||||
sharedViewModel.layoutChangedEvent.value = Event(true)
|
||||
}
|
||||
|
||||
private var shouldTabsBeVisibleDependingOnDestination = true
|
||||
private var shouldTabsBeVisibleDueToOrientationAndKeyboard = true
|
||||
|
||||
private val authenticationRequestedEvent: MutableLiveData<Event<AuthInfo>> by lazy {
|
||||
MutableLiveData<Event<AuthInfo>>()
|
||||
}
|
||||
private var authenticationRequiredDialog: Dialog? = null
|
||||
|
||||
private val coreListener: CoreListenerStub = object : CoreListenerStub() {
|
||||
override fun onAuthenticationRequested(core: Core, authInfo: AuthInfo, method: AuthMethod) {
|
||||
if (authInfo.username == null || authInfo.domain == null || authInfo.realm == null) {
|
||||
return
|
||||
}
|
||||
|
||||
Log.w(
|
||||
"[Main Activity] Authentication requested for account [${authInfo.username}@${authInfo.domain}] with realm [${authInfo.realm}] using method [$method]"
|
||||
)
|
||||
authenticationRequestedEvent.value = Event(authInfo)
|
||||
}
|
||||
}
|
||||
|
||||
private val keyboardVisibilityListeners = arrayListOf<AppUtils.KeyboardVisibilityListener>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Must be done before the setContentView
|
||||
installSplashScreen()
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = ViewModelProvider(this)[SharedMainViewModel::class.java]
|
||||
binding.viewModel = sharedViewModel
|
||||
|
||||
callOverlayViewModel = ViewModelProvider(this)[CallOverlayViewModel::class.java]
|
||||
binding.callOverlayViewModel = callOverlayViewModel
|
||||
|
||||
sharedViewModel.toggleDrawerEvent.observe(
|
||||
this
|
||||
) {
|
||||
it.consume {
|
||||
if (binding.sideMenu.isDrawerOpen(Gravity.LEFT)) {
|
||||
binding.sideMenu.closeDrawer(binding.sideMenuContent, true)
|
||||
} else {
|
||||
binding.sideMenu.openDrawer(binding.sideMenuContent, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coreContext.callErrorMessageResourceId.observe(
|
||||
this
|
||||
) {
|
||||
it.consume { message ->
|
||||
showSnackBar(message)
|
||||
}
|
||||
}
|
||||
|
||||
authenticationRequestedEvent.observe(
|
||||
this
|
||||
) {
|
||||
it.consume { authInfo ->
|
||||
showAuthenticationRequestedDialog(authInfo)
|
||||
}
|
||||
}
|
||||
|
||||
if (coreContext.core.accountList.isEmpty()) {
|
||||
if (corePreferences.firstStart) {
|
||||
startActivity(Intent(this, AssistantActivity::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
tabsFragment = findViewById(R.id.tabs_fragment)
|
||||
statusFragment = findViewById(R.id.status_fragment)
|
||||
|
||||
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?) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
if (intent != null) {
|
||||
Log.d("[Main Activity] Found new intent")
|
||||
handleIntentParams(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
coreContext.contactsManager.addListener(listener)
|
||||
coreContext.core.addListener(coreListener)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
coreContext.core.removeListener(coreListener)
|
||||
coreContext.contactsManager.removeListener(listener)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun showSnackBar(@StringRes resourceId: Int) {
|
||||
Snackbar.make(findViewById(R.id.coordinator), resourceId, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun showSnackBar(@StringRes resourceId: Int, action: Int, listener: () -> Unit) {
|
||||
Snackbar
|
||||
.make(findViewById(R.id.coordinator), resourceId, Snackbar.LENGTH_LONG)
|
||||
.setAction(action) {
|
||||
Log.i("[Snack Bar] Action listener triggered")
|
||||
listener()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun showSnackBar(message: String) {
|
||||
Snackbar.make(findViewById(R.id.coordinator), message, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
super.onPostCreate(savedInstanceState)
|
||||
|
||||
registerComponentCallbacks(componentCallbacks)
|
||||
findNavController(R.id.nav_host_fragment).addOnDestinationChangedListener(this)
|
||||
|
||||
binding.rootCoordinatorLayout.setKeyboardInsetListener { keyboardVisible ->
|
||||
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
|
||||
Log.i(
|
||||
"[Main Activity] Keyboard is ${if (keyboardVisible) "visible" else "invisible"}, orientation is ${if (portraitOrientation) "portrait" else "landscape"}"
|
||||
)
|
||||
shouldTabsBeVisibleDueToOrientationAndKeyboard = !portraitOrientation || !keyboardVisible
|
||||
updateTabsFragmentVisibility()
|
||||
|
||||
for (listener in keyboardVisibilityListeners) {
|
||||
listener.onKeyboardVisibilityChanged(keyboardVisible)
|
||||
}
|
||||
}
|
||||
|
||||
initOverlay()
|
||||
|
||||
if (intent != null) {
|
||||
Log.d("[Main Activity] Found post create intent")
|
||||
handleIntentParams(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
findNavController(R.id.nav_host_fragment).removeOnDestinationChangedListener(this)
|
||||
unregisterComponentCallbacks(componentCallbacks)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onDestinationChanged(
|
||||
controller: NavController,
|
||||
destination: NavDestination,
|
||||
arguments: Bundle?
|
||||
) {
|
||||
hideKeyboard()
|
||||
if (statusFragment.visibility == View.GONE) {
|
||||
statusFragment.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
shouldTabsBeVisibleDependingOnDestination = when (destination.id) {
|
||||
R.id.masterCallLogsFragment, R.id.masterContactsFragment, R.id.dialerFragment, R.id.masterChatRoomsFragment ->
|
||||
true
|
||||
else -> false
|
||||
}
|
||||
updateTabsFragmentVisibility()
|
||||
}
|
||||
|
||||
fun addKeyboardVisibilityListener(listener: AppUtils.KeyboardVisibilityListener) {
|
||||
keyboardVisibilityListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeKeyboardVisibilityListener(listener: AppUtils.KeyboardVisibilityListener) {
|
||||
keyboardVisibilityListeners.remove(listener)
|
||||
}
|
||||
|
||||
fun hideKeyboard() {
|
||||
currentFocus?.hideKeyboard()
|
||||
}
|
||||
|
||||
fun showKeyboard() {
|
||||
// Requires a text field to have the focus
|
||||
if (currentFocus != null) {
|
||||
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
|
||||
.showSoftInput(currentFocus, 0)
|
||||
} else {
|
||||
Log.w("[Main Activity] Can't show the keyboard, no focused view")
|
||||
}
|
||||
}
|
||||
|
||||
fun hideStatusFragment(hide: Boolean) {
|
||||
statusFragment.visibility = if (hide) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
private fun updateTabsFragmentVisibility() {
|
||||
tabsFragment.visibility = if (shouldTabsBeVisibleDependingOnDestination && shouldTabsBeVisibleDueToOrientationAndKeyboard) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun handleIntentParams(intent: Intent) {
|
||||
Log.i(
|
||||
"[Main Activity] Handling intent with action [${intent.action}], type [${intent.type}] and data [${intent.data}]"
|
||||
)
|
||||
|
||||
when (intent.action) {
|
||||
Intent.ACTION_MAIN -> handleMainIntent(intent)
|
||||
Intent.ACTION_SEND, Intent.ACTION_SENDTO -> {
|
||||
if (intent.type == "text/plain") {
|
||||
handleSendText(intent)
|
||||
} else {
|
||||
lifecycleScope.launch {
|
||||
handleSendFile(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
Intent.ACTION_SEND_MULTIPLE -> {
|
||||
lifecycleScope.launch {
|
||||
handleSendMultipleFiles(intent)
|
||||
}
|
||||
}
|
||||
Intent.ACTION_VIEW -> {
|
||||
val uri = intent.data
|
||||
if (uri != null) {
|
||||
if (
|
||||
intent.type == AppUtils.getString(R.string.linphone_address_mime_type) &&
|
||||
PermissionHelper.get().hasReadContactsPermission()
|
||||
) {
|
||||
val contactId =
|
||||
coreContext.contactsManager.getAndroidContactIdFromUri(uri)
|
||||
if (contactId != null) {
|
||||
Log.i("[Main Activity] Found contact URI parameter in intent: $uri")
|
||||
navigateToContact(contactId)
|
||||
}
|
||||
} else {
|
||||
val stringUri = uri.toString()
|
||||
if (stringUri.startsWith("linphone-config:")) {
|
||||
val remoteConfigUri = stringUri.substring("linphone-config:".length)
|
||||
if (corePreferences.autoRemoteProvisioningOnConfigUriHandler) {
|
||||
Log.w(
|
||||
"[Main Activity] Remote provisioning URL set to [$remoteConfigUri], restarting Core now"
|
||||
)
|
||||
applyRemoteProvisioning(remoteConfigUri)
|
||||
} else {
|
||||
Log.i(
|
||||
"[Main Activity] Remote provisioning URL found [$remoteConfigUri], asking for user validation"
|
||||
)
|
||||
showAcceptRemoteConfigurationDialog(remoteConfigUri)
|
||||
}
|
||||
} else {
|
||||
handleTelOrSipUri(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Intent.ACTION_DIAL, Intent.ACTION_CALL -> {
|
||||
val uri = intent.data
|
||||
if (uri != null) {
|
||||
handleTelOrSipUri(uri)
|
||||
}
|
||||
}
|
||||
Intent.ACTION_VIEW_LOCUS -> {
|
||||
if (corePreferences.disableChat) return
|
||||
val locus = Compatibility.extractLocusIdFromIntent(intent)
|
||||
if (locus != null) {
|
||||
Log.i("[Main Activity] Found chat room locus intent extra: $locus")
|
||||
handleLocusOrShortcut(locus)
|
||||
}
|
||||
}
|
||||
else -> handleMainIntent(intent)
|
||||
}
|
||||
|
||||
// Prevent this intent to be processed again
|
||||
intent.action = null
|
||||
intent.data = null
|
||||
val extras = intent.extras
|
||||
if (extras != null) {
|
||||
for (key in extras.keySet()) {
|
||||
intent.removeExtra(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMainIntent(intent: Intent) {
|
||||
when {
|
||||
intent.hasExtra("ContactId") -> {
|
||||
val id = intent.getStringExtra("ContactId")
|
||||
Log.i("[Main Activity] Found contact ID in extras: $id")
|
||||
navigateToContact(id)
|
||||
}
|
||||
intent.hasExtra("Chat") -> {
|
||||
if (corePreferences.disableChat) return
|
||||
|
||||
if (intent.hasExtra("RemoteSipUri") && intent.hasExtra("LocalSipUri")) {
|
||||
val peerAddress = intent.getStringExtra("RemoteSipUri")
|
||||
val localAddress = intent.getStringExtra("LocalSipUri")
|
||||
Log.i(
|
||||
"[Main Activity] Found chat room intent extra: local SIP URI=[$localAddress], peer SIP URI=[$peerAddress]"
|
||||
)
|
||||
navigateToChatRoom(localAddress, peerAddress)
|
||||
} else {
|
||||
Log.i("[Main Activity] Found chat intent extra, go to chat rooms list")
|
||||
navigateToChatRooms()
|
||||
}
|
||||
}
|
||||
intent.hasExtra("Dialer") -> {
|
||||
Log.i("[Main Activity] Found dialer intent extra, go to dialer")
|
||||
val isTransfer = intent.getBooleanExtra("Transfer", false)
|
||||
sharedViewModel.pendingCallTransfer = isTransfer
|
||||
navigateToDialer()
|
||||
}
|
||||
intent.hasExtra("Contacts") -> {
|
||||
Log.i("[Main Activity] Found contacts intent extra, go to contacts list")
|
||||
val isTransfer = intent.getBooleanExtra("Transfer", false)
|
||||
sharedViewModel.pendingCallTransfer = isTransfer
|
||||
navigateToContacts()
|
||||
}
|
||||
else -> {
|
||||
val core = coreContext.core
|
||||
val call = core.currentCall ?: core.calls.firstOrNull()
|
||||
if (call != null) {
|
||||
Log.i(
|
||||
"[Main Activity] Launcher clicked while there is at least one active call, go to CallActivity"
|
||||
)
|
||||
val callIntent = Intent(
|
||||
this,
|
||||
org.linphone.activities.voip.CallActivity::class.java
|
||||
)
|
||||
callIntent.addFlags(
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
|
||||
)
|
||||
startActivity(callIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleTelOrSipUri(uri: Uri) {
|
||||
Log.i("[Main Activity] Found uri: $uri to call")
|
||||
val stringUri = uri.toString()
|
||||
var addressToCall: String = stringUri
|
||||
|
||||
when {
|
||||
addressToCall.startsWith("tel:") -> {
|
||||
Log.i("[Main Activity] Removing tel: prefix")
|
||||
addressToCall = addressToCall.substring("tel:".length)
|
||||
}
|
||||
addressToCall.startsWith("linphone:") -> {
|
||||
Log.i("[Main Activity] Removing linphone: prefix")
|
||||
addressToCall = addressToCall.substring("linphone:".length)
|
||||
}
|
||||
addressToCall.startsWith("sip-linphone:") -> {
|
||||
Log.i("[Main Activity] Removing linphone: sip-linphone")
|
||||
addressToCall = addressToCall.substring("sip-linphone:".length)
|
||||
}
|
||||
}
|
||||
|
||||
addressToCall = addressToCall.replace("%40", "@")
|
||||
|
||||
val address = coreContext.core.interpretUrl(
|
||||
addressToCall,
|
||||
LinphoneUtils.applyInternationalPrefix()
|
||||
)
|
||||
if (address != null) {
|
||||
addressToCall = address.asStringUriOnly()
|
||||
}
|
||||
|
||||
Log.i("[Main Activity] Starting dialer with pre-filled URI $addressToCall")
|
||||
val args = Bundle()
|
||||
args.putString("URI", addressToCall)
|
||||
navigateToDialer(args)
|
||||
}
|
||||
|
||||
private fun handleSendText(intent: Intent) {
|
||||
if (corePreferences.disableChat) return
|
||||
|
||||
intent.getStringExtra(Intent.EXTRA_TEXT)?.let {
|
||||
sharedViewModel.textToShare.value = it
|
||||
}
|
||||
|
||||
handleSendChatRoom(intent)
|
||||
}
|
||||
|
||||
private suspend fun handleSendFile(intent: Intent) {
|
||||
if (corePreferences.disableChat) return
|
||||
|
||||
Log.i("[Main Activity] Found single file to share with type ${intent.type}")
|
||||
|
||||
(intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
|
||||
val list = arrayListOf<String>()
|
||||
coroutineScope {
|
||||
val deferred = async {
|
||||
FileUtils.getFilePath(this@MainActivity, it)
|
||||
}
|
||||
val path = deferred.await()
|
||||
if (path != null) {
|
||||
list.add(path)
|
||||
Log.i("[Main Activity] Found single file to share: $path")
|
||||
}
|
||||
}
|
||||
sharedViewModel.filesToShare.value = list
|
||||
}
|
||||
|
||||
// Check that the current fragment hasn't already handled the event on filesToShare
|
||||
// If it has, don't go further.
|
||||
// For example this may happen when picking a GIF from the keyboard while inside a chat room
|
||||
if (!sharedViewModel.filesToShare.value.isNullOrEmpty()) {
|
||||
handleSendChatRoom(intent)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleSendMultipleFiles(intent: Intent) {
|
||||
if (corePreferences.disableChat) return
|
||||
|
||||
intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM)?.let {
|
||||
val list = arrayListOf<String>()
|
||||
coroutineScope {
|
||||
val deferred = arrayListOf<Deferred<String?>>()
|
||||
for (parcelable in it) {
|
||||
val uri = parcelable as Uri
|
||||
deferred.add(async { FileUtils.getFilePath(this@MainActivity, uri) })
|
||||
}
|
||||
val paths = deferred.awaitAll()
|
||||
for (path in paths) {
|
||||
Log.i("[Main Activity] Found file to share: $path")
|
||||
if (path != null) list.add(path)
|
||||
}
|
||||
}
|
||||
sharedViewModel.filesToShare.value = list
|
||||
}
|
||||
|
||||
handleSendChatRoom(intent)
|
||||
}
|
||||
|
||||
private fun handleSendChatRoom(intent: Intent) {
|
||||
if (corePreferences.disableChat) return
|
||||
|
||||
val uri = intent.data
|
||||
if (uri != null) {
|
||||
Log.i("[Main Activity] Found uri: $uri to send a message to")
|
||||
val stringUri = uri.toString()
|
||||
var addressToIM: String = stringUri
|
||||
try {
|
||||
addressToIM = URLDecoder.decode(stringUri, "UTF-8")
|
||||
} catch (e: UnsupportedEncodingException) {
|
||||
Log.e("[Main Activity] UnsupportedEncodingException: $e")
|
||||
}
|
||||
|
||||
when {
|
||||
addressToIM.startsWith("sms:") ->
|
||||
addressToIM = addressToIM.substring("sms:".length)
|
||||
addressToIM.startsWith("smsto:") ->
|
||||
addressToIM = addressToIM.substring("smsto:".length)
|
||||
addressToIM.startsWith("mms:") ->
|
||||
addressToIM = addressToIM.substring("mms:".length)
|
||||
addressToIM.startsWith("mmsto:") ->
|
||||
addressToIM = addressToIM.substring("mmsto:".length)
|
||||
}
|
||||
|
||||
val localAddress =
|
||||
coreContext.core.defaultAccount?.params?.identityAddress?.asStringUriOnly()
|
||||
val peerAddress = coreContext.core.interpretUrl(
|
||||
addressToIM,
|
||||
LinphoneUtils.applyInternationalPrefix()
|
||||
)?.asStringUriOnly()
|
||||
Log.i(
|
||||
"[Main Activity] Navigating to chat room with local [$localAddress] and peer [$peerAddress] addresses"
|
||||
)
|
||||
navigateToChatRoom(localAddress, peerAddress)
|
||||
} else {
|
||||
val shortcutId = intent.getStringExtra("android.intent.extra.shortcut.ID") // Intent.EXTRA_SHORTCUT_ID
|
||||
if (shortcutId != null) {
|
||||
Log.i("[Main Activity] Found shortcut ID: $shortcutId")
|
||||
handleLocusOrShortcut(shortcutId)
|
||||
} else {
|
||||
Log.i("[Main Activity] Going into chat rooms list")
|
||||
navigateToChatRooms()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleLocusOrShortcut(id: String) {
|
||||
val split = id.split("~")
|
||||
if (split.size == 2) {
|
||||
val localAddress = split[0]
|
||||
val peerAddress = split[1]
|
||||
Log.i(
|
||||
"[Main Activity] Navigating to chat room with local [$localAddress] and peer [$peerAddress] addresses, computed from shortcut/locus id"
|
||||
)
|
||||
navigateToChatRoom(localAddress, peerAddress)
|
||||
} else {
|
||||
Log.e(
|
||||
"[Main Activity] Failed to parse shortcut/locus id: $id, going to chat rooms list"
|
||||
)
|
||||
navigateToChatRooms()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initOverlay() {
|
||||
overlay = binding.root.findViewById(R.id.call_overlay)
|
||||
val callOverlay = overlay
|
||||
callOverlay ?: return
|
||||
|
||||
callOverlay.setOnTouchListener { view, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
overlayX = view.x - event.rawX
|
||||
overlayY = view.y - event.rawY
|
||||
initPosX = view.x
|
||||
initPosY = view.y
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
view.animate()
|
||||
.x(event.rawX + overlayX)
|
||||
.y(event.rawY + overlayY)
|
||||
.setDuration(0)
|
||||
.start()
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (abs(initPosX - view.x) < CorePreferences.OVERLAY_CLICK_SENSITIVITY &&
|
||||
abs(initPosY - view.y) < CorePreferences.OVERLAY_CLICK_SENSITIVITY
|
||||
) {
|
||||
view.performClick()
|
||||
}
|
||||
}
|
||||
else -> return@setOnTouchListener false
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
callOverlay.setOnClickListener {
|
||||
coreContext.onCallOverlayClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyRemoteProvisioning(remoteConfigUri: String) {
|
||||
coreContext.core.provisioningUri = remoteConfigUri
|
||||
coreContext.core.stop()
|
||||
coreContext.core.start()
|
||||
}
|
||||
|
||||
private fun showAcceptRemoteConfigurationDialog(remoteConfigUri: String) {
|
||||
val dialogViewModel = DialogViewModel(
|
||||
remoteConfigUri,
|
||||
getString(R.string.dialog_apply_remote_provisioning_title)
|
||||
)
|
||||
val dialog = DialogUtils.getDialog(this, dialogViewModel)
|
||||
|
||||
dialogViewModel.showCancelButton {
|
||||
Log.i("[Main Activity] User cancelled remote provisioning config")
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
val okLabel = getString(
|
||||
R.string.dialog_apply_remote_provisioning_button
|
||||
)
|
||||
dialogViewModel.showOkButton(
|
||||
{
|
||||
Log.w(
|
||||
"[Main Activity] Remote provisioning URL set to [$remoteConfigUri], restarting Core now"
|
||||
)
|
||||
applyRemoteProvisioning(remoteConfigUri)
|
||||
dialog.dismiss()
|
||||
},
|
||||
okLabel
|
||||
)
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun showAuthenticationRequestedDialog(
|
||||
authInfo: AuthInfo
|
||||
) {
|
||||
authenticationRequiredDialog?.dismiss()
|
||||
|
||||
val accountFound = coreContext.core.accountList.find {
|
||||
it.params.identityAddress?.username == authInfo.username && it.params.identityAddress?.domain == authInfo.domain
|
||||
}
|
||||
if (accountFound == null) {
|
||||
Log.w("[Main Activity] Failed to find account matching auth info, aborting auth dialog")
|
||||
return
|
||||
}
|
||||
|
||||
val identity = "${authInfo.username}@${authInfo.domain}"
|
||||
Log.i("[Main Activity] Showing authentication required dialog for account [$identity]")
|
||||
|
||||
val dialogViewModel = DialogViewModel(
|
||||
getString(R.string.dialog_authentication_required_message, identity),
|
||||
getString(R.string.dialog_authentication_required_title)
|
||||
)
|
||||
dialogViewModel.showPassword = true
|
||||
dialogViewModel.passwordTitle = getString(
|
||||
R.string.settings_password_protection_dialog_input_hint
|
||||
)
|
||||
val dialog = DialogUtils.getDialog(this, dialogViewModel)
|
||||
|
||||
dialogViewModel.showCancelButton {
|
||||
dialog.dismiss()
|
||||
authenticationRequiredDialog = null
|
||||
}
|
||||
|
||||
dialogViewModel.showOkButton(
|
||||
{
|
||||
Log.i(
|
||||
"[Main Activity] Updating password for account [$identity] using auth info [$authInfo]"
|
||||
)
|
||||
val newPassword = dialogViewModel.password
|
||||
authInfo.password = newPassword
|
||||
coreContext.core.addAuthInfo(authInfo)
|
||||
|
||||
coreContext.core.refreshRegisters()
|
||||
|
||||
dialog.dismiss()
|
||||
authenticationRequiredDialog = null
|
||||
},
|
||||
getString(R.string.dialog_authentication_required_change_password_label)
|
||||
)
|
||||
|
||||
dialog.show()
|
||||
authenticationRequiredDialog = dialog
|
||||
}
|
||||
}
|
||||
|
|
@ -1,81 +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.main.about
|
||||
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AboutFragmentBinding
|
||||
|
||||
class AboutFragment : SecureFragment<AboutFragmentBinding>() {
|
||||
private lateinit var viewModel: AboutViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.about_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
viewModel = ViewModelProvider(this)[AboutViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setPrivacyPolicyClickListener {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.about_privacy_policy_link))
|
||||
)
|
||||
try {
|
||||
startActivity(browserIntent)
|
||||
} catch (se: SecurityException) {
|
||||
Log.e("[About] Failed to start browser intent, $se")
|
||||
}
|
||||
}
|
||||
|
||||
binding.setLicenseClickListener {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.about_license_link))
|
||||
)
|
||||
try {
|
||||
startActivity(browserIntent)
|
||||
} catch (se: SecurityException) {
|
||||
Log.e("[About] Failed to start browser intent, $se")
|
||||
}
|
||||
}
|
||||
|
||||
binding.setWeblateClickListener {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.about_weblate_link))
|
||||
)
|
||||
try {
|
||||
startActivity(browserIntent)
|
||||
} catch (se: SecurityException) {
|
||||
Log.e("[About] Failed to start browser intent, $se")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +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.main.about
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
|
||||
class AboutViewModel : ViewModel() {
|
||||
val appVersion: String = coreContext.appVersion
|
||||
|
||||
val sdkVersion: String = coreContext.sdkVersion
|
||||
}
|
||||
|
|
@ -1,40 +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.main.adapters
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||
|
||||
abstract class SelectionListAdapter<T, VH : RecyclerView.ViewHolder>(
|
||||
selectionVM: ListTopBarViewModel,
|
||||
diff: DiffUtil.ItemCallback<T>
|
||||
) :
|
||||
ListAdapter<T, VH>(diff) {
|
||||
|
||||
private var _selectionViewModel: ListTopBarViewModel? = selectionVM
|
||||
protected val selectionViewModel get() = _selectionViewModel!!
|
||||
|
||||
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onDetachedFromRecyclerView(recyclerView)
|
||||
_selectionViewModel = null
|
||||
}
|
||||
}
|
||||
|
|
@ -1,95 +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.main.chat
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
internal abstract class ChatScrollListener(private val mLayoutManager: LinearLayoutManager) :
|
||||
RecyclerView.OnScrollListener() {
|
||||
// The total number of items in the data set after the last load
|
||||
private var previousTotalItemCount = 0
|
||||
|
||||
// True if we are still waiting for the last set of data to load.
|
||||
private var loading = true
|
||||
|
||||
private var userHasScrolledUp: Boolean = false
|
||||
|
||||
// This happens many times a second during a scroll, so be wary of the code you place here.
|
||||
// We are given a few useful parameters to help us work out if we need to load some more data,
|
||||
// but first we check if we are waiting for the previous load to finish.
|
||||
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
||||
val totalItemCount = mLayoutManager.itemCount
|
||||
val firstVisibleItemPosition: Int = mLayoutManager.findFirstVisibleItemPosition()
|
||||
val lastVisibleItemPosition: Int = mLayoutManager.findLastVisibleItemPosition()
|
||||
|
||||
// If the total item count is zero and the previous isn't, assume the
|
||||
// list is invalidated and should be reset back to initial state
|
||||
if (totalItemCount < previousTotalItemCount) {
|
||||
previousTotalItemCount = totalItemCount
|
||||
if (totalItemCount == 0) {
|
||||
loading = true
|
||||
}
|
||||
}
|
||||
|
||||
// If it’s still loading, we check to see if the data set count has
|
||||
// changed, if so we conclude it has finished loading and update the current page
|
||||
// number and total item count.
|
||||
if (loading && totalItemCount > previousTotalItemCount) {
|
||||
loading = false
|
||||
previousTotalItemCount = totalItemCount
|
||||
}
|
||||
|
||||
userHasScrolledUp = lastVisibleItemPosition != totalItemCount - 1
|
||||
if (userHasScrolledUp) {
|
||||
onScrolledUp()
|
||||
} else {
|
||||
onScrolledToEnd()
|
||||
}
|
||||
|
||||
// If it isn’t currently loading, we check to see if we have breached
|
||||
// the mVisibleThreshold and need to reload more data.
|
||||
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
|
||||
// threshold should reflect how many total columns there are too
|
||||
if (!loading &&
|
||||
firstVisibleItemPosition < mVisibleThreshold &&
|
||||
firstVisibleItemPosition >= 0 &&
|
||||
lastVisibleItemPosition < totalItemCount - mVisibleThreshold
|
||||
) {
|
||||
onLoadMore(totalItemCount)
|
||||
loading = true
|
||||
}
|
||||
}
|
||||
|
||||
// Defines the process for actually loading more data based on page
|
||||
protected abstract fun onLoadMore(totalItemsCount: Int)
|
||||
|
||||
// Called when user has started to scroll up, opposed to onScrolledToEnd()
|
||||
protected abstract fun onScrolledUp()
|
||||
|
||||
// Called when user has scrolled and reached the end of the items
|
||||
protected abstract fun onScrolledToEnd()
|
||||
|
||||
companion object {
|
||||
// The minimum amount of items to have below your current scroll position
|
||||
// before loading more.
|
||||
private const val mVisibleThreshold = 5
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +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.main.chat
|
||||
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.core.ChatRoom
|
||||
|
||||
data class GroupChatRoomMember(
|
||||
val address: Address,
|
||||
var isAdmin: Boolean = false,
|
||||
val securityLevel: ChatRoom.SecurityLevel = ChatRoom.SecurityLevel.ClearText,
|
||||
val hasLimeX3DHCapability: Boolean = false,
|
||||
// A participant not yet added to a group can't be set admin at the same time it's added
|
||||
val canBeSetAdmin: Boolean = false
|
||||
)
|
||||
|
|
@ -1,656 +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.main.chat.adapters
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.PopupWindow
|
||||
import android.widget.TextView
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlin.math.abs
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.adapters.SelectionListAdapter
|
||||
import org.linphone.activities.main.chat.data.ChatMessageData
|
||||
import org.linphone.activities.main.chat.data.EventData
|
||||
import org.linphone.activities.main.chat.data.EventLogData
|
||||
import org.linphone.activities.main.chat.data.OnContentClickedListener
|
||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatEventListCellBinding
|
||||
import org.linphone.databinding.ChatMessageListCellBinding
|
||||
import org.linphone.databinding.ChatMessageLongPressMenuBindingImpl
|
||||
import org.linphone.databinding.ChatUnreadMessagesListHeaderBinding
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.HeaderAdapter
|
||||
|
||||
class ChatMessagesListAdapter(
|
||||
selectionVM: ListTopBarViewModel,
|
||||
private val viewLifecycleOwner: LifecycleOwner
|
||||
) : SelectionListAdapter<EventLogData, RecyclerView.ViewHolder>(
|
||||
selectionVM,
|
||||
ChatMessageDiffCallback()
|
||||
),
|
||||
HeaderAdapter {
|
||||
companion object {
|
||||
const val MAX_TIME_TO_GROUP_MESSAGES = 60 // 1 minute
|
||||
}
|
||||
|
||||
val resendMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val deleteMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val forwardMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val replyMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val showImdnForMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val addSipUriToContactEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val openContentEvent: MutableLiveData<Event<Content>> by lazy {
|
||||
MutableLiveData<Event<Content>>()
|
||||
}
|
||||
|
||||
val urlClickEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val sipUriClickedEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val callConferenceEvent: MutableLiveData<Event<Pair<String, String?>>> by lazy {
|
||||
MutableLiveData<Event<Pair<String, String?>>>()
|
||||
}
|
||||
|
||||
val scrollToChatMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val showReactionsListEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val errorEvent: MutableLiveData<Event<Int>> by lazy {
|
||||
MutableLiveData<Event<Int>>()
|
||||
}
|
||||
|
||||
private val contentClickedListener = object : OnContentClickedListener {
|
||||
override fun onContentClicked(content: Content) {
|
||||
openContentEvent.value = Event(content)
|
||||
}
|
||||
|
||||
override fun onWebUrlClicked(url: String) {
|
||||
if (popup?.isShowing == true) {
|
||||
Log.w(
|
||||
"[Chat Message Data] Long press that displayed context menu detected, aborting click on URL [$url]"
|
||||
)
|
||||
return
|
||||
}
|
||||
val urlWithScheme = if (!url.startsWith("http")) "http://$url" else url
|
||||
urlClickEvent.value = Event(urlWithScheme)
|
||||
}
|
||||
|
||||
override fun onSipAddressClicked(sipUri: String) {
|
||||
if (popup?.isShowing == true) {
|
||||
Log.w(
|
||||
"[Chat Message Data] Long press that displayed context menu detected, aborting click on SIP URI [$sipUri]"
|
||||
)
|
||||
return
|
||||
}
|
||||
sipUriClickedEvent.value = Event(sipUri)
|
||||
}
|
||||
|
||||
override fun onEmailAddressClicked(email: String) {
|
||||
if (popup?.isShowing == true) {
|
||||
Log.w(
|
||||
"[Chat Message Data] Long press that displayed context menu detected, aborting click on email address [$email]"
|
||||
)
|
||||
return
|
||||
}
|
||||
val urlWithScheme = if (!email.startsWith("mailto:")) "mailto:$email" else email
|
||||
urlClickEvent.value = Event(urlWithScheme)
|
||||
}
|
||||
|
||||
override fun onCallConference(address: String, subject: String?) {
|
||||
callConferenceEvent.value = Event(Pair(address, subject))
|
||||
}
|
||||
|
||||
override fun onShowReactionsList(chatMessage: ChatMessage) {
|
||||
showReactionsListEvent.value = Event(chatMessage)
|
||||
}
|
||||
|
||||
override fun onError(messageId: Int) {
|
||||
errorEvent.value = Event(messageId)
|
||||
}
|
||||
}
|
||||
|
||||
private var advancedContextMenuOptionsDisabled: Boolean = false
|
||||
private var popup: PopupWindow? = null
|
||||
|
||||
private var unreadMessagesCount: Int = 0
|
||||
private var firstUnreadMessagePosition: Int = -1
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
EventLog.Type.ConferenceChatMessage.toInt() -> createChatMessageViewHolder(parent)
|
||||
else -> createEventViewHolder(parent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createChatMessageViewHolder(parent: ViewGroup): ChatMessageViewHolder {
|
||||
val binding: ChatMessageListCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_message_list_cell,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return ChatMessageViewHolder(binding)
|
||||
}
|
||||
|
||||
private fun createEventViewHolder(parent: ViewGroup): EventViewHolder {
|
||||
val binding: ChatEventListCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_event_list_cell,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return EventViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
val eventLog = getItem(position)
|
||||
when (holder) {
|
||||
is ChatMessageViewHolder -> holder.bind(eventLog)
|
||||
is EventViewHolder -> holder.bind(eventLog)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val eventLog = getItem(position)
|
||||
return eventLog.eventLog.type.toInt()
|
||||
}
|
||||
|
||||
override fun onCurrentListChanged(
|
||||
previousList: MutableList<EventLogData>,
|
||||
currentList: MutableList<EventLogData>
|
||||
) {
|
||||
Log.i(
|
||||
"[Chat Messages Adapter] List has changed, clearing previous first unread message position"
|
||||
)
|
||||
// Need to wait for messages to be added before computing new first unread message position
|
||||
firstUnreadMessagePosition = -1
|
||||
}
|
||||
|
||||
override fun displayHeaderForPosition(position: Int): Boolean {
|
||||
Log.i(
|
||||
"[Chat Messages Adapter] Unread message count is [$unreadMessagesCount], first unread message position is [$firstUnreadMessagePosition]"
|
||||
)
|
||||
if (unreadMessagesCount > 0 && firstUnreadMessagePosition == -1) {
|
||||
computeFirstUnreadMessagePosition()
|
||||
}
|
||||
return position == firstUnreadMessagePosition
|
||||
}
|
||||
|
||||
override fun getHeaderViewForPosition(context: Context, position: Int): View {
|
||||
val binding: ChatUnreadMessagesListHeaderBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context),
|
||||
R.layout.chat_unread_messages_list_header,
|
||||
null,
|
||||
false
|
||||
)
|
||||
binding.title = AppUtils.getStringWithPlural(
|
||||
R.plurals.chat_room_unread_messages_event,
|
||||
unreadMessagesCount
|
||||
)
|
||||
binding.executePendingBindings()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
fun disableAdvancedContextMenuOptions() {
|
||||
advancedContextMenuOptionsDisabled = true
|
||||
}
|
||||
|
||||
fun setUnreadMessageCount(count: Int, forceUpdate: Boolean) {
|
||||
Log.i("[Chat Messages Adapter] [$count] unread message in chat room")
|
||||
// Once list has been filled once, don't show the unread message header
|
||||
// when new messages are added to the history whilst it is visible
|
||||
unreadMessagesCount = if (itemCount == 0 || forceUpdate) count else 0
|
||||
firstUnreadMessagePosition = -1
|
||||
Log.i(
|
||||
"[Chat Messages Adapter] Set [$unreadMessagesCount] unread message(s) for current chat room"
|
||||
)
|
||||
}
|
||||
|
||||
fun getFirstUnreadMessagePosition(): Int {
|
||||
Log.i(
|
||||
"[Chat Messages Adapter] First unread message position is [$firstUnreadMessagePosition]"
|
||||
)
|
||||
return firstUnreadMessagePosition
|
||||
}
|
||||
|
||||
private fun computeFirstUnreadMessagePosition() {
|
||||
Log.i(
|
||||
"[Chat Messages Adapter] [$unreadMessagesCount] unread message(s) for current chat room"
|
||||
)
|
||||
if (unreadMessagesCount > 0) {
|
||||
Log.i("[Chat Messages Adapter] Computing first unread message position")
|
||||
var messageCount = 0
|
||||
for (position in itemCount - 1 downTo 0) {
|
||||
val eventLog = getItem(position)
|
||||
val data = eventLog.data
|
||||
if (data is ChatMessageData) {
|
||||
messageCount += 1
|
||||
if (messageCount == unreadMessagesCount) {
|
||||
firstUnreadMessagePosition = position
|
||||
Log.i(
|
||||
"[Chat Messages Adapter] First unread message position found [$firstUnreadMessagePosition]"
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ChatMessageViewHolder(
|
||||
val binding: ChatMessageListCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(eventLog: EventLogData) {
|
||||
with(binding) {
|
||||
if (eventLog.eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val chatMessageData = eventLog.data as ChatMessageData
|
||||
chatMessageData.setContentClickListener(contentClickedListener)
|
||||
|
||||
val chatMessage = chatMessageData.chatMessage
|
||||
data = chatMessageData
|
||||
|
||||
chatMessageData.contactNewlyFoundEvent.observe(viewLifecycleOwner) {
|
||||
it.consume {
|
||||
// Post to prevent IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling
|
||||
binding.root.post {
|
||||
try {
|
||||
notifyItemChanged(bindingAdapterPosition)
|
||||
} catch (e: Exception) {
|
||||
Log.e(
|
||||
"[Chat Messages Adapter] Can't notify item [$bindingAdapterPosition] has changed: $e"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
// This is for item selection through ListTopBarFragment
|
||||
selectionListViewModel = selectionViewModel
|
||||
selectionViewModel.isEditionEnabled.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
position = bindingAdapterPosition
|
||||
}
|
||||
|
||||
setClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
setReplyClickListener {
|
||||
val reply = chatMessageData.replyData.value?.chatMessage
|
||||
if (reply != null) {
|
||||
scrollToChatMessageEvent.value = Event(reply)
|
||||
}
|
||||
}
|
||||
|
||||
// Grouping
|
||||
var hasPrevious = false
|
||||
var hasNext = false
|
||||
|
||||
if (bindingAdapterPosition > 0) {
|
||||
val previousItem = getItem(bindingAdapterPosition - 1)
|
||||
if (previousItem.eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val previousMessage = previousItem.eventLog.chatMessage
|
||||
if (previousMessage != null && previousMessage.fromAddress.weakEqual(
|
||||
chatMessage.fromAddress
|
||||
)
|
||||
) {
|
||||
if (abs(chatMessage.time - previousMessage.time) < MAX_TIME_TO_GROUP_MESSAGES) {
|
||||
hasPrevious = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bindingAdapterPosition >= 0 && bindingAdapterPosition < itemCount - 1) {
|
||||
val nextItem = getItem(bindingAdapterPosition + 1)
|
||||
if (nextItem.eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val nextMessage = nextItem.eventLog.chatMessage
|
||||
if (nextMessage != null && nextMessage.fromAddress.weakEqual(
|
||||
chatMessage.fromAddress
|
||||
)
|
||||
) {
|
||||
if (abs(nextMessage.time - chatMessage.time) < MAX_TIME_TO_GROUP_MESSAGES) {
|
||||
hasNext = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chatMessageData.updateBubbleBackground(hasPrevious, hasNext)
|
||||
|
||||
executePendingBindings()
|
||||
|
||||
setContextMenuClickListener {
|
||||
val popupView: ChatMessageLongPressMenuBindingImpl = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(root.context),
|
||||
R.layout.chat_message_long_press_menu,
|
||||
null,
|
||||
false
|
||||
)
|
||||
|
||||
val itemSize = AppUtils.getDimension(R.dimen.chat_message_popup_item_height).toInt()
|
||||
var totalSize = itemSize * 8
|
||||
if (chatMessage.chatRoom.hasCapability(
|
||||
ChatRoom.Capabilities.OneToOne.toInt()
|
||||
)
|
||||
) {
|
||||
// No message id
|
||||
popupView.imdnHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
if (chatMessage.state != ChatMessage.State.NotDelivered) {
|
||||
popupView.resendHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
if (chatMessage.contents.find { content -> content.isText } == null) {
|
||||
popupView.copyTextHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
if (chatMessage.isOutgoing ||
|
||||
chatMessageData.contact.value != null ||
|
||||
advancedContextMenuOptionsDisabled ||
|
||||
corePreferences.readOnlyNativeContacts
|
||||
) {
|
||||
popupView.addToContactsHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
if (chatMessage.chatRoom.isReadOnly) {
|
||||
popupView.replyHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
if (advancedContextMenuOptionsDisabled) {
|
||||
popupView.forwardHidden = true
|
||||
totalSize -= itemSize
|
||||
}
|
||||
|
||||
val reaction = chatMessage.ownReaction
|
||||
if (reaction != null) {
|
||||
when (reaction.body) {
|
||||
AppUtils.getString(R.string.emoji_love) -> {
|
||||
popupView.heartSelected = true
|
||||
}
|
||||
AppUtils.getString(R.string.emoji_laughing) -> {
|
||||
popupView.laughingSelected = true
|
||||
}
|
||||
AppUtils.getString(R.string.emoji_surprised) -> {
|
||||
popupView.surprisedSelected = true
|
||||
}
|
||||
AppUtils.getString(R.string.emoji_thumbs_up) -> {
|
||||
popupView.thumbsUpSelected = true
|
||||
}
|
||||
AppUtils.getString(R.string.emoji_tear) -> {
|
||||
popupView.cryingSelected = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When using WRAP_CONTENT instead of real size, fails to place the
|
||||
// popup window above if not enough space is available below
|
||||
val popupWindow = PopupWindow(
|
||||
popupView.root,
|
||||
AppUtils.getDimension(R.dimen.chat_message_popup_width).toInt(),
|
||||
totalSize,
|
||||
true
|
||||
)
|
||||
popup = popupWindow
|
||||
|
||||
// Elevation is for showing a shadow around the popup
|
||||
popupWindow.elevation = 20f
|
||||
|
||||
popupView.setEmojiClickListener {
|
||||
val emoji = it as? TextView
|
||||
if (emoji != null) {
|
||||
reactToMessage(emoji.text.toString())
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
}
|
||||
popupView.setResendClickListener {
|
||||
resendMessage()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setCopyTextClickListener {
|
||||
copyTextToClipboard()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setForwardClickListener {
|
||||
forwardMessage()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setReplyClickListener {
|
||||
replyMessage()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setImdnClickListener {
|
||||
showImdnDeliveryFragment()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setAddToContactsClickListener {
|
||||
addSenderToContacts()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
popupView.setDeleteClickListener {
|
||||
deleteMessage()
|
||||
popupWindow.dismiss()
|
||||
}
|
||||
|
||||
val gravity = if (chatMessage.isOutgoing) Gravity.END else Gravity.START
|
||||
popupWindow.showAsDropDown(background, 0, 0, gravity or Gravity.TOP)
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun reactToMessage(reaction: String) {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
val ownReaction = chatMessage.ownReaction
|
||||
if (ownReaction != null && ownReaction.body == reaction) {
|
||||
Log.i(
|
||||
"[Chat Message Data] Removing our reaction to message [$chatMessage] (previously [$reaction])"
|
||||
)
|
||||
// Empty string means remove existing reaction
|
||||
val reactionMessage = chatMessage.createReaction("")
|
||||
reactionMessage.send()
|
||||
} else {
|
||||
Log.i(
|
||||
"[Chat Message Data] Reacting to message [$chatMessage] with [$reaction] emoji"
|
||||
)
|
||||
val reactionMessage = chatMessage.createReaction(reaction)
|
||||
reactionMessage.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun resendMessage() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
chatMessage.userData = bindingAdapterPosition
|
||||
resendMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyTextToClipboard() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
val content = chatMessage.contents.find { content -> content.isText }
|
||||
if (content != null) {
|
||||
val clipboard: ClipboardManager =
|
||||
coreContext.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Message", content.utf8Text)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun forwardMessage() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
forwardMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun replyMessage() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
replyMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showImdnDeliveryFragment() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
showImdnForMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteMessage() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
chatMessage.userData = bindingAdapterPosition
|
||||
deleteMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addSenderToContacts() {
|
||||
val chatMessage = binding.data?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
val copy = chatMessage.fromAddress.clone()
|
||||
copy.clean() // To remove gruu if any
|
||||
addSipUriToContactEvent.value = Event(copy.asStringUriOnly())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class EventViewHolder(
|
||||
private val binding: ChatEventListCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(eventLog: EventLogData) {
|
||||
with(binding) {
|
||||
val eventViewModel = eventLog.data as EventData
|
||||
data = eventViewModel
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
// This is for item selection through ListTopBarFragment
|
||||
selectionListViewModel = selectionViewModel
|
||||
selectionViewModel.isEditionEnabled.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
position = bindingAdapterPosition
|
||||
}
|
||||
|
||||
binding.setClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ChatMessageDiffCallback : DiffUtil.ItemCallback<EventLogData>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: EventLogData,
|
||||
newItem: EventLogData
|
||||
): Boolean {
|
||||
return if (oldItem.type == EventLog.Type.ConferenceChatMessage &&
|
||||
newItem.type == EventLog.Type.ConferenceChatMessage
|
||||
) {
|
||||
val oldData = (oldItem.data as ChatMessageData)
|
||||
val newData = (newItem.data as ChatMessageData)
|
||||
|
||||
oldData.time.value == newData.time.value &&
|
||||
oldData.isOutgoing == newData.isOutgoing
|
||||
} else {
|
||||
oldItem.notifyId == newItem.notifyId
|
||||
}
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: EventLogData,
|
||||
newItem: EventLogData
|
||||
): Boolean {
|
||||
return if (oldItem.type == EventLog.Type.ConferenceChatMessage &&
|
||||
newItem.type == EventLog.Type.ConferenceChatMessage
|
||||
) {
|
||||
val oldData = (oldItem.data as ChatMessageData)
|
||||
val newData = (newItem.data as ChatMessageData)
|
||||
|
||||
val previous = oldData.hasPreviousMessage == newData.hasPreviousMessage
|
||||
val next = oldData.hasNextMessage == newData.hasNextMessage
|
||||
val isDisplayed = newData.isDisplayed.value == true
|
||||
isDisplayed && previous && next
|
||||
} else {
|
||||
oldItem.type != EventLog.Type.ConferenceChatMessage &&
|
||||
newItem.type != EventLog.Type.ConferenceChatMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,139 +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.main.chat.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.adapters.SelectionListAdapter
|
||||
import org.linphone.activities.main.chat.data.ChatRoomData
|
||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomListCellBinding
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class ChatRoomsListAdapter(
|
||||
selectionVM: ListTopBarViewModel,
|
||||
private val viewLifecycleOwner: LifecycleOwner
|
||||
) : SelectionListAdapter<ChatRoomData, RecyclerView.ViewHolder>(selectionVM, ChatRoomDiffCallback()) {
|
||||
val selectedChatRoomEvent: MutableLiveData<Event<ChatRoom>> by lazy {
|
||||
MutableLiveData<Event<ChatRoom>>()
|
||||
}
|
||||
|
||||
private var isForwardPending = false
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding: ChatRoomListCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_room_list_cell,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
(holder as ViewHolder).bind(getItem(position))
|
||||
}
|
||||
|
||||
fun forwardPending(pending: Boolean) {
|
||||
isForwardPending = pending
|
||||
notifyItemRangeChanged(0, itemCount)
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ChatRoomListCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(chatRoomData: ChatRoomData) {
|
||||
with(binding) {
|
||||
chatRoomData.update()
|
||||
data = chatRoomData
|
||||
|
||||
chatRoomData.contactNewlyFoundEvent.observe(viewLifecycleOwner) {
|
||||
it.consume {
|
||||
// Post to prevent IllegalStateException: Cannot call this method while RecyclerView is computing a layout or scrolling
|
||||
binding.root.post {
|
||||
try {
|
||||
notifyItemChanged(bindingAdapterPosition)
|
||||
} catch (e: Exception) {
|
||||
Log.e(
|
||||
"[Chat Rooms Adapter] Can't notify item [$bindingAdapterPosition] has changed: $e"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
// This is for item selection through ListTopBarFragment
|
||||
selectionListViewModel = selectionViewModel
|
||||
selectionViewModel.isEditionEnabled.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
position = bindingAdapterPosition
|
||||
}
|
||||
|
||||
forwardPending = isForwardPending
|
||||
|
||||
setClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||
} else {
|
||||
selectedChatRoomEvent.value = Event(chatRoomData.chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
setLongClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == false) {
|
||||
selectionViewModel.isEditionEnabled.value = true
|
||||
// Selection will be handled by click listener
|
||||
true
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ChatRoomDiffCallback : DiffUtil.ItemCallback<ChatRoomData>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ChatRoomData,
|
||||
newItem: ChatRoomData
|
||||
): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ChatRoomData,
|
||||
newItem: ChatRoomData
|
||||
): Boolean {
|
||||
return false // To force redraw
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +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.main.chat.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.GroupChatRoomMember
|
||||
import org.linphone.activities.main.chat.data.GroupInfoParticipantData
|
||||
import org.linphone.databinding.ChatRoomGroupInfoParticipantCellBinding
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class GroupInfoParticipantsAdapter(
|
||||
private val viewLifecycleOwner: LifecycleOwner,
|
||||
private val isEncryptionEnabled: Boolean
|
||||
) : ListAdapter<GroupInfoParticipantData, RecyclerView.ViewHolder>(ParticipantDiffCallback()) {
|
||||
private var showAdmin: Boolean = false
|
||||
|
||||
val participantRemovedEvent: MutableLiveData<Event<GroupChatRoomMember>> by lazy {
|
||||
MutableLiveData<Event<GroupChatRoomMember>>()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding: ChatRoomGroupInfoParticipantCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_room_group_info_participant_cell,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
(holder as ViewHolder).bind(getItem(position))
|
||||
}
|
||||
|
||||
fun showAdminControls(show: Boolean) {
|
||||
showAdmin = show
|
||||
notifyItemRangeChanged(0, itemCount)
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
val binding: ChatRoomGroupInfoParticipantCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(participantViewModel: GroupInfoParticipantData) {
|
||||
with(binding) {
|
||||
participantViewModel.showAdminControls.value = showAdmin
|
||||
data = participantViewModel
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
setRemoveClickListener {
|
||||
participantRemovedEvent.value = Event(participantViewModel.participant)
|
||||
}
|
||||
isEncrypted = isEncryptionEnabled
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ParticipantDiffCallback : DiffUtil.ItemCallback<GroupInfoParticipantData>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: GroupInfoParticipantData,
|
||||
newItem: GroupInfoParticipantData
|
||||
): Boolean {
|
||||
return oldItem.sipUri == newItem.sipUri
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: GroupInfoParticipantData,
|
||||
newItem: GroupInfoParticipantData
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,130 +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.main.chat.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.data.ImdnParticipantData
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.databinding.ChatRoomImdnParticipantCellBinding
|
||||
import org.linphone.databinding.ImdnListHeaderBinding
|
||||
import org.linphone.utils.HeaderAdapter
|
||||
|
||||
class ImdnAdapter(
|
||||
private val viewLifecycleOwner: LifecycleOwner
|
||||
) : ListAdapter<ImdnParticipantData, RecyclerView.ViewHolder>(ParticipantImdnStateDiffCallback()), HeaderAdapter {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding: ChatRoomImdnParticipantCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_room_imdn_participant_cell,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
(holder as ViewHolder).bind(getItem(position))
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
val binding: ChatRoomImdnParticipantCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(participantImdnData: ImdnParticipantData) {
|
||||
with(binding) {
|
||||
data = participantImdnData
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun displayHeaderForPosition(position: Int): Boolean {
|
||||
if (position >= itemCount) return false
|
||||
val participantImdnState = getItem(position)
|
||||
val previousPosition = position - 1
|
||||
return if (previousPosition >= 0) {
|
||||
getItem(previousPosition).imdnState.state != participantImdnState.imdnState.state
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun getHeaderViewForPosition(context: Context, position: Int): View {
|
||||
val participantImdnState = getItem(position).imdnState
|
||||
val binding: ImdnListHeaderBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context),
|
||||
R.layout.imdn_list_header,
|
||||
null,
|
||||
false
|
||||
)
|
||||
when (participantImdnState.state) {
|
||||
ChatMessage.State.Displayed -> {
|
||||
binding.title = R.string.chat_message_imdn_displayed
|
||||
binding.textColor = R.color.imdn_read_color
|
||||
binding.icon = R.drawable.chat_read
|
||||
}
|
||||
ChatMessage.State.DeliveredToUser -> {
|
||||
binding.title = R.string.chat_message_imdn_delivered
|
||||
binding.textColor = R.color.grey_color
|
||||
binding.icon = R.drawable.chat_delivered
|
||||
}
|
||||
ChatMessage.State.Delivered -> {
|
||||
binding.title = R.string.chat_message_imdn_sent
|
||||
binding.textColor = R.color.grey_color
|
||||
binding.icon = R.drawable.chat_delivered
|
||||
}
|
||||
ChatMessage.State.NotDelivered -> {
|
||||
binding.title = R.string.chat_message_imdn_undelivered
|
||||
binding.textColor = R.color.red_color
|
||||
binding.icon = R.drawable.chat_error
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
binding.executePendingBindings()
|
||||
return binding.root
|
||||
}
|
||||
}
|
||||
|
||||
private class ParticipantImdnStateDiffCallback : DiffUtil.ItemCallback<ImdnParticipantData>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ImdnParticipantData,
|
||||
newItem: ImdnParticipantData
|
||||
): Boolean {
|
||||
return oldItem.sipUri == newItem.sipUri
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ImdnParticipantData,
|
||||
newItem: ImdnParticipantData
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +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.main.chat.data
|
||||
|
||||
import android.webkit.MimeTypeMap
|
||||
import org.linphone.utils.FileUtils
|
||||
|
||||
class ChatMessageAttachmentData(
|
||||
val path: String,
|
||||
private val deleteCallback: (attachment: ChatMessageAttachmentData) -> Unit
|
||||
) {
|
||||
val fileName: String = FileUtils.getNameFromFilePath(path)
|
||||
val isImage: Boolean
|
||||
val isVideo: Boolean
|
||||
val isAudio: Boolean
|
||||
val isPdf: Boolean
|
||||
|
||||
init {
|
||||
val extension = FileUtils.getExtensionFromFileName(path)
|
||||
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||
val mimeType = FileUtils.getMimeType(mime)
|
||||
isImage = mimeType == FileUtils.MimeType.Image
|
||||
isVideo = mimeType == FileUtils.MimeType.Video
|
||||
isAudio = mimeType == FileUtils.MimeType.Audio
|
||||
isPdf = mimeType == FileUtils.MimeType.Pdf
|
||||
}
|
||||
|
||||
fun delete() {
|
||||
deleteCallback(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,560 +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.main.chat.data
|
||||
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.style.UnderlineSpan
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.media.AudioFocusRequestCompat
|
||||
import java.io.BufferedReader
|
||||
import java.io.FileReader
|
||||
import java.lang.StringBuilder
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.AudioRouteUtils
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ChatMessageContentData(
|
||||
private val chatMessage: ChatMessage,
|
||||
private val contentIndex: Int
|
||||
) {
|
||||
var listener: OnContentClickedListener? = null
|
||||
|
||||
val isOutgoing = chatMessage.isOutgoing
|
||||
|
||||
val isImage = MutableLiveData<Boolean>()
|
||||
val isVideo = MutableLiveData<Boolean>()
|
||||
val isAudio = MutableLiveData<Boolean>()
|
||||
val isPdf = MutableLiveData<Boolean>()
|
||||
val isGenericFile = MutableLiveData<Boolean>()
|
||||
val isVoiceRecording = MutableLiveData<Boolean>()
|
||||
val isConferenceSchedule = MutableLiveData<Boolean>()
|
||||
val isConferenceUpdated = MutableLiveData<Boolean>()
|
||||
val isConferenceCancelled = MutableLiveData<Boolean>()
|
||||
val isBroadcast = MutableLiveData<Boolean>()
|
||||
val isSpeaker = MutableLiveData<Boolean>()
|
||||
|
||||
val fileName = MutableLiveData<String>()
|
||||
val filePath = MutableLiveData<String>()
|
||||
|
||||
val downloadable = MutableLiveData<Boolean>()
|
||||
val fileTransferProgress = MutableLiveData<Boolean>()
|
||||
val fileTransferProgressInt = MutableLiveData<Int>()
|
||||
val downloadLabel = MutableLiveData<Spannable>()
|
||||
|
||||
val voiceRecordDuration = MutableLiveData<Int>()
|
||||
val formattedDuration = MutableLiveData<String>()
|
||||
val voiceRecordPlayingPosition = MutableLiveData<Int>()
|
||||
val isVoiceRecordPlaying = MutableLiveData<Boolean>()
|
||||
|
||||
val conferenceSubject = MutableLiveData<String>()
|
||||
val conferenceDescription = MutableLiveData<String>()
|
||||
val conferenceParticipantCount = MutableLiveData<String>()
|
||||
val conferenceDate = MutableLiveData<String>()
|
||||
val conferenceTime = MutableLiveData<String>()
|
||||
val conferenceDuration = MutableLiveData<String>()
|
||||
val showDuration = MutableLiveData<Boolean>()
|
||||
|
||||
val isAlone: Boolean
|
||||
get() {
|
||||
var count = 0
|
||||
for (content in chatMessage.contents) {
|
||||
if (content.isFileTransfer || content.isFile) {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
return count == 1
|
||||
}
|
||||
|
||||
private var isFileEncrypted: Boolean = false
|
||||
|
||||
private var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
|
||||
|
||||
private lateinit var voiceRecordingPlayer: Player
|
||||
private val playerListener = PlayerListener {
|
||||
Log.i("[Voice Recording] End of file reached")
|
||||
stopVoiceRecording()
|
||||
}
|
||||
|
||||
private var conferenceAddress: String? = null
|
||||
|
||||
private fun getContent(): Content {
|
||||
return chatMessage.contents[contentIndex]
|
||||
}
|
||||
|
||||
private val chatMessageListener: ChatMessageListenerStub = object : ChatMessageListenerStub() {
|
||||
override fun onFileTransferProgressIndication(
|
||||
message: ChatMessage,
|
||||
c: Content,
|
||||
offset: Int,
|
||||
total: Int
|
||||
) {
|
||||
if (c.filePath == getContent().filePath) {
|
||||
if (fileTransferProgress.value == false) {
|
||||
fileTransferProgress.value = true
|
||||
}
|
||||
val percent = ((offset * 100.0) / total).toInt() // Conversion from int to double and back to int is required
|
||||
Log.d("[Content] Transfer progress is: $offset / $total -> $percent%")
|
||||
fileTransferProgressInt.value = percent
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) {
|
||||
if (state == ChatMessage.State.FileTransferDone || state == ChatMessage.State.FileTransferError) {
|
||||
fileTransferProgress.value = false
|
||||
updateContent()
|
||||
|
||||
if (state == ChatMessage.State.FileTransferDone) {
|
||||
Log.i("[Chat Message] File transfer done")
|
||||
if (!message.isOutgoing && !message.isEphemeral) {
|
||||
Log.i("[Chat Message] Adding content to media store")
|
||||
coreContext.addContentToMediaStore(getContent())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
init {
|
||||
isVoiceRecordPlaying.value = false
|
||||
voiceRecordDuration.value = 0
|
||||
voiceRecordPlayingPosition.value = 0
|
||||
fileTransferProgress.value = false
|
||||
fileTransferProgressInt.value = 0
|
||||
|
||||
updateContent()
|
||||
chatMessage.addListener(chatMessageListener)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
scope.cancel()
|
||||
|
||||
deletePlainFilePath()
|
||||
chatMessage.removeListener(chatMessageListener)
|
||||
|
||||
if (this::voiceRecordingPlayer.isInitialized) {
|
||||
Log.i("[Voice Recording] Destroying voice record")
|
||||
stopVoiceRecording()
|
||||
voiceRecordingPlayer.removeListener(playerListener)
|
||||
}
|
||||
}
|
||||
|
||||
fun download() {
|
||||
if (chatMessage.isFileTransferInProgress) {
|
||||
Log.w(
|
||||
"[Content] Another FileTransfer content for this message is currently being downloaded, can't start another one for now"
|
||||
)
|
||||
listener?.onError(R.string.chat_message_download_already_in_progress)
|
||||
return
|
||||
}
|
||||
|
||||
val content = getContent()
|
||||
val filePath = content.filePath
|
||||
if (content.isFileTransfer) {
|
||||
if (filePath.isNullOrEmpty()) {
|
||||
val contentName = content.name
|
||||
if (contentName != null) {
|
||||
val file = FileUtils.getFileStoragePath(contentName)
|
||||
content.filePath = file.path
|
||||
Log.i("[Content] Started downloading $contentName into ${content.filePath}")
|
||||
} else {
|
||||
Log.e("[Content] Content name is null, can't download it!")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
Log.w(
|
||||
"[Content] File path already set [$filePath] using it (auto download that failed probably)"
|
||||
)
|
||||
}
|
||||
|
||||
if (!chatMessage.downloadContent(content)) {
|
||||
Log.e("[Content] Failed to start content download!")
|
||||
}
|
||||
} else {
|
||||
Log.e("[Content] Content is not a FileTransfer, can't download it!")
|
||||
}
|
||||
}
|
||||
|
||||
fun openFile() {
|
||||
listener?.onContentClicked(getContent())
|
||||
}
|
||||
|
||||
private fun deletePlainFilePath() {
|
||||
val path = filePath.value.orEmpty()
|
||||
if (path.isNotEmpty() && isFileEncrypted) {
|
||||
Log.i("[Content] [VFS] Deleting file used for preview: $path")
|
||||
FileUtils.deleteFile(path)
|
||||
filePath.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateContent() {
|
||||
Log.i("[Content] Updating content")
|
||||
deletePlainFilePath()
|
||||
|
||||
val content = getContent()
|
||||
isFileEncrypted = content.isFileEncrypted
|
||||
Log.i(
|
||||
"[Content] Is ${if (content.isFile) "file" else "file transfer"} content encrypted ? $isFileEncrypted"
|
||||
)
|
||||
|
||||
val contentName = content.name
|
||||
val contentFilePath = content.filePath
|
||||
val name = if (contentName.isNullOrEmpty()) {
|
||||
if (!contentFilePath.isNullOrEmpty()) {
|
||||
FileUtils.getNameFromFilePath(contentFilePath)
|
||||
} else {
|
||||
"<unknown>"
|
||||
}
|
||||
} else {
|
||||
contentName
|
||||
}
|
||||
fileName.value = name
|
||||
filePath.value = ""
|
||||
|
||||
// Display download size and underline text
|
||||
val fileSize = AppUtils.bytesToDisplayableSize(content.fileSize.toLong())
|
||||
val spannable = SpannableString(
|
||||
"${AppUtils.getString(R.string.chat_message_download_file)} ($fileSize)"
|
||||
)
|
||||
spannable.setSpan(UnderlineSpan(), 0, spannable.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
downloadLabel.value = spannable
|
||||
|
||||
isImage.value = false
|
||||
isVideo.value = false
|
||||
isAudio.value = false
|
||||
isPdf.value = false
|
||||
isVoiceRecording.value = false
|
||||
isConferenceSchedule.value = false
|
||||
isConferenceUpdated.value = false
|
||||
isConferenceCancelled.value = false
|
||||
|
||||
if (content.isFile || (content.isFileTransfer && chatMessage.isOutgoing)) {
|
||||
val path = if (isFileEncrypted) {
|
||||
Log.i(
|
||||
"[Content] [VFS] Content is encrypted, requesting plain file path for file [${content.filePath}]"
|
||||
)
|
||||
content.exportPlainFile()
|
||||
} else {
|
||||
content.filePath ?: ""
|
||||
}
|
||||
downloadable.value = content.filePath.orEmpty().isEmpty()
|
||||
|
||||
val isVoiceRecord = content.isVoiceRecording
|
||||
isVoiceRecording.value = isVoiceRecord
|
||||
|
||||
val isConferenceIcs = content.isIcalendar
|
||||
isConferenceSchedule.value = isConferenceIcs
|
||||
|
||||
if (path.isNotEmpty()) {
|
||||
filePath.value = path
|
||||
val extension = FileUtils.getExtensionFromFileName(path)
|
||||
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||
val type = when (FileUtils.getMimeType(mime)) {
|
||||
FileUtils.MimeType.Image -> {
|
||||
isImage.value = true
|
||||
"image"
|
||||
}
|
||||
FileUtils.MimeType.Video -> {
|
||||
isVideo.value = !isVoiceRecord
|
||||
if (isVoiceRecord) "voice recording" else "video"
|
||||
}
|
||||
FileUtils.MimeType.Audio -> {
|
||||
isAudio.value = !isVoiceRecord
|
||||
if (isVoiceRecord) "voice recording" else "audio"
|
||||
}
|
||||
FileUtils.MimeType.Pdf -> {
|
||||
isPdf.value = true
|
||||
"pdf"
|
||||
}
|
||||
else -> {
|
||||
if (isConferenceIcs) "conference invitation" else "unknown"
|
||||
}
|
||||
}
|
||||
Log.i(
|
||||
"[Content] Extension for file [$path] is [$extension], deduced type from MIME is [$type]"
|
||||
)
|
||||
|
||||
if (isVoiceRecord) {
|
||||
val duration = content.fileDuration // duration is in ms
|
||||
voiceRecordDuration.value = duration
|
||||
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(
|
||||
duration
|
||||
)
|
||||
Log.i(
|
||||
"[Content] Voice recording duration is ${voiceRecordDuration.value} ($duration)"
|
||||
)
|
||||
} else if (isConferenceIcs) {
|
||||
parseConferenceInvite(content)
|
||||
}
|
||||
} else if (isConferenceIcs) {
|
||||
Log.i("[Content] Found content with icalendar file")
|
||||
parseConferenceInvite(content)
|
||||
} else {
|
||||
Log.w(
|
||||
"[Content] Found ${if (content.isFile) "file" else "file transfer"} content with empty path..."
|
||||
)
|
||||
}
|
||||
} else if (content.isFileTransfer) {
|
||||
downloadable.value = true
|
||||
val extension = FileUtils.getExtensionFromFileName(name)
|
||||
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||
when (FileUtils.getMimeType(mime)) {
|
||||
FileUtils.MimeType.Image -> {
|
||||
isImage.value = true
|
||||
}
|
||||
FileUtils.MimeType.Video -> {
|
||||
isVideo.value = true
|
||||
}
|
||||
FileUtils.MimeType.Audio -> {
|
||||
isAudio.value = true
|
||||
}
|
||||
FileUtils.MimeType.Pdf -> {
|
||||
isPdf.value = true
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
} else if (content.isIcalendar) {
|
||||
Log.i("[Content] Found content with icalendar body")
|
||||
isConferenceSchedule.value = true
|
||||
parseConferenceInvite(content)
|
||||
} else {
|
||||
Log.w("[Content] Found content that's neither a file or a file transfer")
|
||||
}
|
||||
|
||||
isGenericFile.value = !isPdf.value!! && !isAudio.value!! && !isVideo.value!! && !isImage.value!! && !isVoiceRecording.value!! && !isConferenceSchedule.value!!
|
||||
}
|
||||
|
||||
private fun parseConferenceInvite(content: Content) {
|
||||
val conferenceInfo = Factory.instance().createConferenceInfoFromIcalendarContent(content)
|
||||
val conferenceUri = conferenceInfo?.uri?.asStringUriOnly() ?: ""
|
||||
if (conferenceInfo != null && conferenceUri.isNotEmpty()) {
|
||||
conferenceAddress = conferenceUri
|
||||
Log.i(
|
||||
"[Content] Created conference info from ICS with address $conferenceAddress"
|
||||
)
|
||||
conferenceSubject.value = conferenceInfo.subject
|
||||
conferenceDescription.value = conferenceInfo.description
|
||||
|
||||
val state = conferenceInfo.state
|
||||
isConferenceUpdated.value = state == ConferenceInfo.State.Updated
|
||||
isConferenceCancelled.value = state == ConferenceInfo.State.Cancelled
|
||||
|
||||
conferenceDate.value = TimestampUtils.dateToString(conferenceInfo.dateTime)
|
||||
conferenceTime.value = TimestampUtils.timeToString(conferenceInfo.dateTime)
|
||||
|
||||
val minutes = conferenceInfo.duration
|
||||
val hours = TimeUnit.MINUTES.toHours(minutes.toLong())
|
||||
val remainMinutes = minutes - TimeUnit.HOURS.toMinutes(hours).toInt()
|
||||
conferenceDuration.value = TimestampUtils.durationToString(hours.toInt(), remainMinutes)
|
||||
showDuration.value = minutes > 0
|
||||
|
||||
// Check if organizer is part of participants list
|
||||
var participantsCount = conferenceInfo.participants.size
|
||||
val organizer = conferenceInfo.organizer
|
||||
var organizerFound = false
|
||||
var allSpeaker = true
|
||||
isSpeaker.value = true
|
||||
for (info in conferenceInfo.participantInfos) {
|
||||
val participant = info.address
|
||||
if (participant.weakEqual(chatMessage.chatRoom.localAddress)) {
|
||||
isSpeaker.value = info.role == Participant.Role.Speaker
|
||||
}
|
||||
|
||||
if (info.role == Participant.Role.Listener) {
|
||||
allSpeaker = false
|
||||
}
|
||||
|
||||
if (organizer != null) {
|
||||
if (participant.weakEqual(organizer)) {
|
||||
organizerFound = true
|
||||
}
|
||||
}
|
||||
}
|
||||
isBroadcast.value = allSpeaker == false
|
||||
|
||||
if (!organizerFound) participantsCount += 1 // +1 for organizer
|
||||
conferenceParticipantCount.value = String.format(
|
||||
AppUtils.getString(R.string.conference_invite_participants_count),
|
||||
participantsCount
|
||||
)
|
||||
} else if (conferenceInfo == null) {
|
||||
if (content.filePath != null) {
|
||||
try {
|
||||
val br = BufferedReader(FileReader(content.filePath))
|
||||
var line: String?
|
||||
val textBuilder = StringBuilder()
|
||||
while (br.readLine().also { line = it } != null) {
|
||||
textBuilder.append(line)
|
||||
textBuilder.append('\n')
|
||||
}
|
||||
br.close()
|
||||
Log.e(
|
||||
"[Content] Failed to create conference info from ICS file [${content.filePath}]: $textBuilder"
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("[Content] Failed to read content of ICS file [${content.filePath}]: $e")
|
||||
}
|
||||
} else {
|
||||
Log.e("[Content] Failed to create conference info from ICS: ${content.utf8Text}")
|
||||
}
|
||||
} else if (conferenceInfo.uri == null) {
|
||||
Log.e(
|
||||
"[Content] Failed to find the conference URI in conference info [$conferenceInfo]"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun callConferenceAddress() {
|
||||
val address = conferenceAddress
|
||||
if (address == null) {
|
||||
Log.e("[Content] Can't call null conference address!")
|
||||
return
|
||||
}
|
||||
listener?.onCallConference(address, conferenceSubject.value)
|
||||
}
|
||||
|
||||
/** Voice recording specifics */
|
||||
|
||||
fun playVoiceRecording() {
|
||||
Log.i("[Voice Recording] Playing voice record")
|
||||
if (isPlayerClosed()) {
|
||||
Log.w("[Voice Recording] Player closed, let's open it first")
|
||||
initVoiceRecordPlayer()
|
||||
}
|
||||
|
||||
if (AppUtils.isMediaVolumeLow(coreContext.context)) {
|
||||
Toast.makeText(
|
||||
coreContext.context,
|
||||
R.string.chat_message_voice_recording_playback_low_volume,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
if (voiceRecordAudioFocusRequest == null) {
|
||||
voiceRecordAudioFocusRequest = AppUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
|
||||
coreContext.context
|
||||
)
|
||||
}
|
||||
voiceRecordingPlayer.start()
|
||||
isVoiceRecordPlaying.value = true
|
||||
tickerFlow().onEach {
|
||||
withContext(Dispatchers.Main) {
|
||||
voiceRecordPlayingPosition.value = voiceRecordingPlayer.currentPosition
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
fun pauseVoiceRecording() {
|
||||
Log.i("[Voice Recording] Pausing voice record")
|
||||
if (!isPlayerClosed()) {
|
||||
voiceRecordingPlayer.pause()
|
||||
}
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AppUtils.releaseAudioFocusForVoiceRecordingOrPlayback(coreContext.context, request)
|
||||
voiceRecordAudioFocusRequest = null
|
||||
}
|
||||
|
||||
isVoiceRecordPlaying.value = false
|
||||
}
|
||||
|
||||
private fun tickerFlow() = flow {
|
||||
while (isVoiceRecordPlaying.value == true) {
|
||||
emit(Unit)
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initVoiceRecordPlayer() {
|
||||
Log.i("[Voice Recording] Creating player for voice record")
|
||||
val playbackSoundCard = AudioRouteUtils.getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage()
|
||||
Log.i(
|
||||
"[Voice Recording] Using device $playbackSoundCard to make the voice message playback"
|
||||
)
|
||||
|
||||
val localPlayer = coreContext.core.createLocalPlayer(playbackSoundCard, null, null)
|
||||
if (localPlayer != null) {
|
||||
voiceRecordingPlayer = localPlayer
|
||||
} else {
|
||||
Log.e("[Voice Recording] Couldn't create local player!")
|
||||
return
|
||||
}
|
||||
voiceRecordingPlayer.addListener(playerListener)
|
||||
|
||||
val path = filePath.value
|
||||
voiceRecordingPlayer.open(path.orEmpty())
|
||||
voiceRecordDuration.value = voiceRecordingPlayer.duration
|
||||
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(
|
||||
voiceRecordingPlayer.duration
|
||||
) // is already in milliseconds
|
||||
Log.i(
|
||||
"[Voice Recording] Duration is ${voiceRecordDuration.value} (${voiceRecordingPlayer.duration})"
|
||||
)
|
||||
}
|
||||
|
||||
private fun stopVoiceRecording() {
|
||||
if (!isPlayerClosed()) {
|
||||
Log.i("[Voice Recording] Stopping voice record")
|
||||
pauseVoiceRecording()
|
||||
voiceRecordingPlayer.seek(0)
|
||||
voiceRecordPlayingPosition.value = 0
|
||||
voiceRecordingPlayer.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPlayerClosed(): Boolean {
|
||||
return !this::voiceRecordingPlayer.isInitialized || voiceRecordingPlayer.state == Player.State.Closed
|
||||
}
|
||||
}
|
||||
|
||||
interface OnContentClickedListener {
|
||||
fun onContentClicked(content: Content)
|
||||
|
||||
fun onSipAddressClicked(sipUri: String)
|
||||
|
||||
fun onEmailAddressClicked(email: String)
|
||||
|
||||
fun onWebUrlClicked(url: String)
|
||||
|
||||
fun onCallConference(address: String, subject: String?)
|
||||
|
||||
fun onShowReactionsList(chatMessage: ChatMessage)
|
||||
|
||||
fun onError(messageId: Int)
|
||||
}
|
||||
|
|
@ -1,351 +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.main.chat.data
|
||||
|
||||
import android.os.CountDownTimer
|
||||
import android.text.Spannable
|
||||
import android.util.Patterns
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import java.util.regex.Pattern
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.core.ChatMessageListenerStub
|
||||
import org.linphone.core.ChatMessageReaction
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.PatternClickableSpan
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMessage.fromAddress) {
|
||||
private var contentListener: OnContentClickedListener? = null
|
||||
|
||||
val sendInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val showImdn = MutableLiveData<Boolean>()
|
||||
|
||||
val imdnIcon = MutableLiveData<Int>()
|
||||
|
||||
val backgroundRes = MutableLiveData<Int>()
|
||||
|
||||
val hideAvatar = MutableLiveData<Boolean>()
|
||||
|
||||
val hideTime = MutableLiveData<Boolean>()
|
||||
|
||||
val contents = MutableLiveData<ArrayList<ChatMessageContentData>>()
|
||||
|
||||
val time = MutableLiveData<String>()
|
||||
|
||||
val ephemeralLifetime = MutableLiveData<String>()
|
||||
|
||||
val text = MutableLiveData<Spannable>()
|
||||
|
||||
val isTextEmoji = MutableLiveData<Boolean>()
|
||||
|
||||
val replyData = MutableLiveData<ChatMessageData>()
|
||||
|
||||
val isDisplayed = MutableLiveData<Boolean>()
|
||||
|
||||
val isOutgoing = chatMessage.isOutgoing
|
||||
|
||||
val contactNewlyFoundEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val reactions = MutableLiveData<ArrayList<String>>()
|
||||
|
||||
var hasPreviousMessage = false
|
||||
var hasNextMessage = false
|
||||
|
||||
private var countDownTimer: CountDownTimer? = null
|
||||
|
||||
private val listener = object : ChatMessageListenerStub() {
|
||||
override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) {
|
||||
time.value = TimestampUtils.toString(chatMessage.time)
|
||||
updateChatMessageState(state)
|
||||
}
|
||||
|
||||
override fun onEphemeralMessageTimerStarted(message: ChatMessage) {
|
||||
updateEphemeralTimer()
|
||||
}
|
||||
|
||||
override fun onNewMessageReaction(message: ChatMessage, reaction: ChatMessageReaction) {
|
||||
Log.i(
|
||||
"[Chat Message Data] New reaction to display [${reaction.body}] from [${reaction.fromAddress.asStringUriOnly()}]"
|
||||
)
|
||||
updateReactionsList()
|
||||
}
|
||||
|
||||
override fun onReactionRemoved(message: ChatMessage, address: Address) {
|
||||
Log.i(
|
||||
"[Chat Message Data] [${address.asStringUriOnly()}] removed it's previous reaction"
|
||||
)
|
||||
updateReactionsList()
|
||||
}
|
||||
}
|
||||
|
||||
private val contactsListener = object : ContactsUpdatedListenerStub() {
|
||||
override fun onContactsUpdated() {
|
||||
contactLookup()
|
||||
if (contact.value != null) {
|
||||
coreContext.contactsManager.removeListener(this)
|
||||
contactNewlyFoundEvent.value = Event(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatMessage.addListener(listener)
|
||||
|
||||
backgroundRes.value = if (chatMessage.isOutgoing) R.drawable.chat_bubble_outgoing_full else R.drawable.chat_bubble_incoming_full
|
||||
hideAvatar.value = false
|
||||
|
||||
if (chatMessage.isReply) {
|
||||
val reply = chatMessage.replyMessage
|
||||
if (reply != null) {
|
||||
Log.i(
|
||||
"[Chat Message Data] Message is a reply of message id [${chatMessage.replyMessageId}] sent by [${chatMessage.replyMessageSenderAddress?.asStringUriOnly()}]"
|
||||
)
|
||||
replyData.value = ChatMessageData(reply)
|
||||
}
|
||||
}
|
||||
|
||||
time.value = TimestampUtils.toString(chatMessage.time)
|
||||
updateEphemeralTimer()
|
||||
|
||||
updateChatMessageState(chatMessage.state)
|
||||
updateContentsList()
|
||||
|
||||
if (contact.value == null) {
|
||||
coreContext.contactsManager.addListener(contactsListener)
|
||||
}
|
||||
|
||||
updateReactionsList()
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
super.destroy()
|
||||
|
||||
if (chatMessage.isReply) {
|
||||
replyData.value?.destroy()
|
||||
}
|
||||
|
||||
contents.value.orEmpty().forEach(ChatMessageContentData::destroy)
|
||||
chatMessage.removeListener(listener)
|
||||
contentListener = null
|
||||
}
|
||||
|
||||
fun updateBubbleBackground(hasPrevious: Boolean, hasNext: Boolean) {
|
||||
hasPreviousMessage = hasPrevious
|
||||
hasNextMessage = hasNext
|
||||
hideTime.value = false
|
||||
hideAvatar.value = false
|
||||
|
||||
if (hasPrevious) {
|
||||
hideTime.value = true
|
||||
}
|
||||
|
||||
if (chatMessage.isOutgoing) {
|
||||
if (hasNext && hasPrevious) {
|
||||
backgroundRes.value = R.drawable.chat_bubble_outgoing_split_2
|
||||
} else if (hasNext) {
|
||||
backgroundRes.value = R.drawable.chat_bubble_outgoing_split_1
|
||||
} else if (hasPrevious) {
|
||||
backgroundRes.value = R.drawable.chat_bubble_outgoing_split_3
|
||||
} else {
|
||||
backgroundRes.value = R.drawable.chat_bubble_outgoing_full
|
||||
}
|
||||
} else {
|
||||
if (hasNext && hasPrevious) {
|
||||
hideAvatar.value = true
|
||||
backgroundRes.value = R.drawable.chat_bubble_incoming_split_2
|
||||
} else if (hasNext) {
|
||||
backgroundRes.value = R.drawable.chat_bubble_incoming_split_1
|
||||
} else if (hasPrevious) {
|
||||
hideAvatar.value = true
|
||||
backgroundRes.value = R.drawable.chat_bubble_incoming_split_3
|
||||
} else {
|
||||
backgroundRes.value = R.drawable.chat_bubble_incoming_full
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setContentClickListener(listener: OnContentClickedListener) {
|
||||
contentListener = listener
|
||||
|
||||
for (data in contents.value.orEmpty()) {
|
||||
data.listener = listener
|
||||
}
|
||||
}
|
||||
|
||||
fun showReactionsList() {
|
||||
contentListener?.onShowReactionsList(chatMessage)
|
||||
}
|
||||
|
||||
private fun updateChatMessageState(state: ChatMessage.State) {
|
||||
sendInProgress.value = when (state) {
|
||||
ChatMessage.State.InProgress, ChatMessage.State.FileTransferInProgress, ChatMessage.State.FileTransferDone -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
showImdn.value = when (state) {
|
||||
ChatMessage.State.DeliveredToUser, ChatMessage.State.Displayed,
|
||||
ChatMessage.State.NotDelivered, ChatMessage.State.FileTransferError -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
imdnIcon.value = when (state) {
|
||||
ChatMessage.State.DeliveredToUser -> R.drawable.chat_delivered
|
||||
ChatMessage.State.Displayed -> R.drawable.chat_read
|
||||
ChatMessage.State.FileTransferError, ChatMessage.State.NotDelivered -> R.drawable.chat_error
|
||||
else -> R.drawable.chat_error
|
||||
}
|
||||
|
||||
isDisplayed.value = state == ChatMessage.State.Displayed
|
||||
}
|
||||
|
||||
private fun updateContentsList() {
|
||||
contents.value.orEmpty().forEach(ChatMessageContentData::destroy)
|
||||
val list = arrayListOf<ChatMessageContentData>()
|
||||
|
||||
val contentsList = chatMessage.contents
|
||||
for (index in contentsList.indices) {
|
||||
val content = contentsList[index]
|
||||
if (content.isFileTransfer || content.isFile || content.isIcalendar) {
|
||||
val data = ChatMessageContentData(chatMessage, index)
|
||||
data.listener = contentListener
|
||||
list.add(data)
|
||||
} else if (content.isText) {
|
||||
val textContent = content.utf8Text.orEmpty().trim()
|
||||
val spannable = Spannable.Factory.getInstance().newSpannable(textContent)
|
||||
text.value = PatternClickableSpan()
|
||||
.add(
|
||||
Pattern.compile(
|
||||
"(?:<?sips?:)[a-zA-Z0-9+_.\\-]+(?:@([a-zA-Z0-9+_.\\-;=~]+))+(>)?"
|
||||
),
|
||||
object : PatternClickableSpan.SpannableClickedListener {
|
||||
override fun onSpanClicked(text: String) {
|
||||
Log.i("[Chat Message Data] Clicked on SIP URI: $text")
|
||||
contentListener?.onSipAddressClicked(text)
|
||||
}
|
||||
}
|
||||
)
|
||||
.add(
|
||||
Patterns.EMAIL_ADDRESS,
|
||||
object : PatternClickableSpan.SpannableClickedListener {
|
||||
override fun onSpanClicked(text: String) {
|
||||
Log.i("[Chat Message Data] Clicked on email address: $text")
|
||||
contentListener?.onEmailAddressClicked(text)
|
||||
}
|
||||
}
|
||||
)
|
||||
.add(
|
||||
Patterns.PHONE,
|
||||
object : PatternClickableSpan.SpannableClickedListener {
|
||||
override fun onSpanClicked(text: String) {
|
||||
Log.i("[Chat Message Data] Clicked on phone number: $text")
|
||||
contentListener?.onSipAddressClicked(text)
|
||||
}
|
||||
}
|
||||
)
|
||||
.add(
|
||||
Patterns.WEB_URL,
|
||||
object : PatternClickableSpan.SpannableClickedListener {
|
||||
override fun onSpanClicked(text: String) {
|
||||
Log.i("[Chat Message Data] Clicked on web URL: $text")
|
||||
contentListener?.onWebUrlClicked(text)
|
||||
}
|
||||
}
|
||||
).build(spannable)
|
||||
isTextEmoji.value = AppUtils.isTextOnlyContainingEmoji(textContent)
|
||||
} else {
|
||||
Log.e(
|
||||
"[Chat Message Data] Unexpected content with type: ${content.type}/${content.subtype}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
contents.value = list
|
||||
}
|
||||
|
||||
fun updateReactionsList() {
|
||||
val reactionsList = arrayListOf<String>()
|
||||
val allReactions = chatMessage.reactions
|
||||
|
||||
var sameReactionTwiceOrMore = false
|
||||
if (allReactions.isNotEmpty()) {
|
||||
for (reaction in allReactions) {
|
||||
val body = reaction.body
|
||||
if (!reactionsList.contains(body)) {
|
||||
reactionsList.add(body)
|
||||
} else {
|
||||
sameReactionTwiceOrMore = true
|
||||
}
|
||||
}
|
||||
|
||||
if (sameReactionTwiceOrMore) {
|
||||
reactionsList.add(allReactions.size.toString())
|
||||
}
|
||||
}
|
||||
|
||||
reactions.value = reactionsList
|
||||
}
|
||||
|
||||
private fun updateEphemeralTimer() {
|
||||
if (chatMessage.isEphemeral) {
|
||||
if (chatMessage.ephemeralExpireTime == 0L) {
|
||||
// This means the message hasn't been read by all participants yet, so the countdown hasn't started
|
||||
// In this case we simply display the configured value for lifetime
|
||||
ephemeralLifetime.value = formatLifetime(chatMessage.ephemeralLifetime)
|
||||
} else {
|
||||
// Countdown has started, display remaining time
|
||||
val remaining = chatMessage.ephemeralExpireTime - (System.currentTimeMillis() / 1000)
|
||||
ephemeralLifetime.value = formatLifetime(remaining)
|
||||
if (countDownTimer == null) {
|
||||
countDownTimer = object : CountDownTimer(remaining * 1000, 1000) {
|
||||
override fun onFinish() {}
|
||||
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
ephemeralLifetime.postValue(formatLifetime(millisUntilFinished / 1000))
|
||||
}
|
||||
}
|
||||
countDownTimer?.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatLifetime(seconds: Long): String {
|
||||
val days = seconds / 86400
|
||||
return when {
|
||||
days >= 1L -> AppUtils.getStringWithPlural(R.plurals.days, days.toInt())
|
||||
else -> String.format(
|
||||
"%02d:%02d:%02d",
|
||||
seconds / 3600,
|
||||
(seconds % 3600) / 60,
|
||||
(seconds % 60)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* 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.main.chat.data
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.ChatMessageReaction
|
||||
|
||||
class ChatMessageReactionData(
|
||||
chatMessageReaction: ChatMessageReaction
|
||||
) : GenericContactData(chatMessageReaction.fromAddress) {
|
||||
|
||||
val reaction = MutableLiveData<String>()
|
||||
|
||||
init {
|
||||
reaction.value = chatMessageReaction.body
|
||||
}
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
* 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.main.chat.data
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.core.ChatMessageListenerStub
|
||||
import org.linphone.core.ChatMessageReaction
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class ChatMessageReactionsListData(private val chatMessage: ChatMessage) {
|
||||
val reactions = MutableLiveData<ArrayList<ChatMessageReaction>>()
|
||||
|
||||
val filteredReactions = MutableLiveData<ArrayList<ChatMessageReactionData>>()
|
||||
|
||||
val reactionsMap = HashMap<String, Int>()
|
||||
|
||||
val listener = object : ChatMessageListenerStub() {
|
||||
override fun onNewMessageReaction(message: ChatMessage, reaction: ChatMessageReaction) {
|
||||
val address = reaction.fromAddress
|
||||
Log.i(
|
||||
"[Chat Message Reactions List] Reaction received [${reaction.body}] from [${address.asStringUriOnly()}]"
|
||||
)
|
||||
updateReactionsList(message)
|
||||
}
|
||||
|
||||
override fun onReactionRemoved(message: ChatMessage, address: Address) {
|
||||
Log.i(
|
||||
"[Chat Message Reactions List] Reaction removed by [${address.asStringUriOnly()}]"
|
||||
)
|
||||
updateReactionsList(message)
|
||||
}
|
||||
}
|
||||
|
||||
private var filter = ""
|
||||
|
||||
init {
|
||||
chatMessage.addListener(listener)
|
||||
|
||||
updateReactionsList(chatMessage)
|
||||
}
|
||||
|
||||
fun onDestroy() {
|
||||
chatMessage.removeListener(listener)
|
||||
}
|
||||
|
||||
fun updateFilteredReactions(newFilter: String) {
|
||||
filter = newFilter
|
||||
filteredReactions.value.orEmpty().forEach(ChatMessageReactionData::destroy)
|
||||
|
||||
val reactionsList = arrayListOf<ChatMessageReactionData>()
|
||||
for (reaction in reactions.value.orEmpty()) {
|
||||
if (filter.isEmpty() || filter == reaction.body) {
|
||||
val data = ChatMessageReactionData(reaction)
|
||||
reactionsList.add(data)
|
||||
}
|
||||
}
|
||||
filteredReactions.value = reactionsList
|
||||
}
|
||||
|
||||
private fun updateReactionsList(chatMessage: ChatMessage) {
|
||||
reactionsMap.clear()
|
||||
|
||||
val reactionsList = arrayListOf<ChatMessageReaction>()
|
||||
for (reaction in chatMessage.reactions) {
|
||||
val body = reaction.body
|
||||
val count = if (reactionsMap.containsKey(body)) {
|
||||
reactionsMap[body] ?: 0
|
||||
} else {
|
||||
0
|
||||
}
|
||||
// getOrDefault isn't available for API 23 :'(
|
||||
reactionsMap[body] = count + 1
|
||||
reactionsList.add(reaction)
|
||||
}
|
||||
reactions.value = reactionsList
|
||||
|
||||
updateFilteredReactions(filter)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,277 +0,0 @@
|
|||
/*
|
||||
* 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.main.chat.data
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.style.StyleSpan
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.ContactDataInterface
|
||||
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ChatRoomData(val chatRoom: ChatRoom) : ContactDataInterface {
|
||||
override val contact: MutableLiveData<Friend> = MutableLiveData<Friend>()
|
||||
override val displayName: MutableLiveData<String> = MutableLiveData<String>()
|
||||
override val securityLevel: MutableLiveData<ChatRoom.SecurityLevel> = MutableLiveData<ChatRoom.SecurityLevel>()
|
||||
override val showGroupChatAvatar: Boolean
|
||||
get() = !oneToOneChatRoom
|
||||
override val presenceStatus: MutableLiveData<ConsolidatedPresence> = MutableLiveData<ConsolidatedPresence>()
|
||||
override val coroutineScope: CoroutineScope = coreContext.coroutineScope
|
||||
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
|
||||
val unreadMessagesCount = MutableLiveData<Int>()
|
||||
|
||||
val subject = MutableLiveData<String>()
|
||||
|
||||
val securityLevelIcon = MutableLiveData<Int>()
|
||||
|
||||
val securityLevelContentDescription = MutableLiveData<Int>()
|
||||
|
||||
val ephemeralEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val lastUpdate = MutableLiveData<String>()
|
||||
|
||||
val lastMessageText = MutableLiveData<SpannableStringBuilder>()
|
||||
|
||||
val showLastMessageImdnIcon = MutableLiveData<Boolean>()
|
||||
|
||||
val lastMessageImdnIcon = MutableLiveData<Int>()
|
||||
|
||||
val notificationsMuted = MutableLiveData<Boolean>()
|
||||
|
||||
private val basicChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoom.Capabilities.Basic.toInt())
|
||||
}
|
||||
|
||||
val oneToOneChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())
|
||||
}
|
||||
|
||||
val encryptedChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoom.Capabilities.Encrypted.toInt())
|
||||
}
|
||||
|
||||
val contactNewlyFoundEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
private val contactsListener = object : ContactsUpdatedListenerStub() {
|
||||
override fun onContactsUpdated() {
|
||||
if (contact.value == null && oneToOneChatRoom) {
|
||||
searchMatchingContact()
|
||||
}
|
||||
if (!oneToOneChatRoom) {
|
||||
formatLastMessage(chatRoom.lastMessageInHistory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.contactsManager.addListener(contactsListener)
|
||||
|
||||
lastUpdate.value = "00:00"
|
||||
presenceStatus.value = ConsolidatedPresence.Offline
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
coreContext.contactsManager.removeListener(contactsListener)
|
||||
}
|
||||
|
||||
fun update() {
|
||||
unreadMessagesCount.value = chatRoom.unreadMessagesCount
|
||||
|
||||
subject.value = chatRoom.subject
|
||||
updateSecurityIcon()
|
||||
ephemeralEnabled.value = chatRoom.isEphemeralEnabled
|
||||
|
||||
contactLookup()
|
||||
formatLastMessage(chatRoom.lastMessageInHistory)
|
||||
|
||||
notificationsMuted.value = areNotificationsMuted()
|
||||
}
|
||||
|
||||
fun markAsRead() {
|
||||
chatRoom.markAsRead()
|
||||
unreadMessagesCount.value = 0
|
||||
}
|
||||
|
||||
private fun updateSecurityIcon() {
|
||||
val level = chatRoom.securityLevel
|
||||
securityLevel.value = level
|
||||
|
||||
securityLevelIcon.value = when (level) {
|
||||
ChatRoom.SecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoom.SecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
securityLevelContentDescription.value = when (level) {
|
||||
ChatRoom.SecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoom.SecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
private fun contactLookup() {
|
||||
if (oneToOneChatRoom) {
|
||||
searchMatchingContact()
|
||||
} else {
|
||||
displayName.value = chatRoom.subject ?: chatRoom.peerAddress.asStringUriOnly()
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchMatchingContact() {
|
||||
val remoteAddress = if (basicChatRoom) {
|
||||
chatRoom.peerAddress
|
||||
} else {
|
||||
val participants = chatRoom.participants
|
||||
if (participants.isNotEmpty()) {
|
||||
participants.first().address
|
||||
} else {
|
||||
Log.e(
|
||||
"[Chat Room] ${chatRoom.peerAddress} doesn't have any participant (state ${chatRoom.state})!"
|
||||
)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (remoteAddress != null) {
|
||||
val friend = coreContext.contactsManager.findContactByAddress(remoteAddress)
|
||||
if (friend != null) {
|
||||
val newlyFound = contact.value == null
|
||||
|
||||
contact.value = friend!!
|
||||
presenceStatus.value = friend.consolidatedPresence
|
||||
friend.addListener {
|
||||
presenceStatus.value = it.consolidatedPresence
|
||||
}
|
||||
|
||||
if (newlyFound) {
|
||||
contactNewlyFoundEvent.value = Event(true)
|
||||
}
|
||||
} else {
|
||||
displayName.value = LinphoneUtils.getDisplayName(remoteAddress)
|
||||
}
|
||||
} else {
|
||||
displayName.value = chatRoom.peerAddress.asStringUriOnly()
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatLastMessage(msg: ChatMessage?) {
|
||||
val lastUpdateTime = chatRoom.lastUpdateTime
|
||||
coroutineScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
lastUpdate.postValue(TimestampUtils.toString(lastUpdateTime, true))
|
||||
}
|
||||
}
|
||||
|
||||
val builder = SpannableStringBuilder()
|
||||
if (msg == null) {
|
||||
lastMessageText.value = builder
|
||||
showLastMessageImdnIcon.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (msg.isOutgoing && msg.state != ChatMessage.State.Displayed) {
|
||||
msg.addListener(object : ChatMessageListenerStub() {
|
||||
override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) {
|
||||
computeLastMessageImdnIcon(message)
|
||||
}
|
||||
})
|
||||
}
|
||||
computeLastMessageImdnIcon(msg)
|
||||
|
||||
if (!oneToOneChatRoom) {
|
||||
val sender: String =
|
||||
coreContext.contactsManager.findContactByAddress(msg.fromAddress)?.name
|
||||
?: LinphoneUtils.getDisplayName(msg.fromAddress)
|
||||
builder.append(
|
||||
coreContext.context.getString(R.string.chat_room_last_message_sender_format, sender)
|
||||
)
|
||||
builder.append(" ")
|
||||
}
|
||||
|
||||
for (content in msg.contents) {
|
||||
if (content.isIcalendar) {
|
||||
val body = AppUtils.getString(R.string.conference_invitation)
|
||||
builder.append(body)
|
||||
builder.setSpan(
|
||||
StyleSpan(Typeface.ITALIC),
|
||||
builder.length - body.length,
|
||||
builder.length,
|
||||
0
|
||||
)
|
||||
break
|
||||
} else if (content.isVoiceRecording) {
|
||||
val body = AppUtils.getString(R.string.chat_message_voice_recording)
|
||||
builder.append(body)
|
||||
builder.setSpan(
|
||||
StyleSpan(Typeface.ITALIC),
|
||||
builder.length - body.length,
|
||||
builder.length,
|
||||
0
|
||||
)
|
||||
break
|
||||
} else if (content.isFile || content.isFileTransfer) {
|
||||
builder.append(content.name + " ")
|
||||
} else if (content.isText) {
|
||||
builder.append(content.utf8Text + " ")
|
||||
}
|
||||
}
|
||||
|
||||
builder.trim()
|
||||
lastMessageText.value = builder
|
||||
}
|
||||
|
||||
private fun computeLastMessageImdnIcon(msg: ChatMessage) {
|
||||
val state = msg.state
|
||||
showLastMessageImdnIcon.value = if (msg.isOutgoing) {
|
||||
when (state) {
|
||||
ChatMessage.State.DeliveredToUser, ChatMessage.State.Displayed,
|
||||
ChatMessage.State.NotDelivered, ChatMessage.State.FileTransferError -> true
|
||||
else -> false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
lastMessageImdnIcon.value = when (state) {
|
||||
ChatMessage.State.DeliveredToUser -> R.drawable.chat_delivered
|
||||
ChatMessage.State.Displayed -> R.drawable.chat_read
|
||||
ChatMessage.State.FileTransferError, ChatMessage.State.NotDelivered -> R.drawable.chat_error
|
||||
else -> R.drawable.chat_error
|
||||
}
|
||||
}
|
||||
|
||||
private fun areNotificationsMuted(): Boolean {
|
||||
return chatRoom.muted
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +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.main.chat.data
|
||||
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.ParticipantDevice
|
||||
|
||||
class DevicesListChildData(private val device: ParticipantDevice) {
|
||||
val deviceName: String = device.name.orEmpty()
|
||||
|
||||
val securityLevelIcon: Int by lazy {
|
||||
when (device.securityLevel) {
|
||||
ChatRoom.SecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoom.SecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
}
|
||||
|
||||
val securityContentDescription: Int by lazy {
|
||||
when (device.securityLevel) {
|
||||
ChatRoom.SecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoom.SecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
fun onClick() {
|
||||
coreContext.startCall(device.address, forceZRTP = true)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,78 +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.main.chat.data
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.Participant
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class DevicesListGroupData(private val participant: Participant) : GenericContactData(
|
||||
participant.address
|
||||
) {
|
||||
val securityLevelIcon: Int by lazy {
|
||||
when (participant.securityLevel) {
|
||||
ChatRoom.SecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoom.SecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
}
|
||||
|
||||
val securityLevelContentDescription: Int by lazy {
|
||||
when (participant.securityLevel) {
|
||||
ChatRoom.SecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoom.SecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
val sipUri: String get() = LinphoneUtils.getDisplayableAddress(participant.address)
|
||||
|
||||
val isExpanded = MutableLiveData<Boolean>()
|
||||
|
||||
val devices = MutableLiveData<ArrayList<DevicesListChildData>>()
|
||||
|
||||
init {
|
||||
securityLevel.value = participant.securityLevel
|
||||
isExpanded.value = false
|
||||
|
||||
val list = arrayListOf<DevicesListChildData>()
|
||||
for (device in participant.devices) {
|
||||
list.add(DevicesListChildData((device)))
|
||||
}
|
||||
devices.value = list
|
||||
}
|
||||
|
||||
fun toggleExpanded() {
|
||||
isExpanded.value = isExpanded.value != true
|
||||
}
|
||||
|
||||
fun onClick() {
|
||||
val device = if (participant.devices.isEmpty()) null else participant.devices.first()
|
||||
if (device?.address != null) {
|
||||
coreContext.startCall(device.address, forceZRTP = true)
|
||||
} else {
|
||||
coreContext.startCall(participant.address, forceZRTP = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +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.main.chat.data
|
||||
|
||||
class EphemeralDurationData(
|
||||
val textResource: Int,
|
||||
selectedDuration: Long,
|
||||
private val duration: Long,
|
||||
private val listener: DurationItemClicked
|
||||
) {
|
||||
val selected: Boolean = selectedDuration == duration
|
||||
|
||||
fun setSelected() {
|
||||
listener.onDurationValueChanged(duration)
|
||||
}
|
||||
}
|
||||
|
||||
interface DurationItemClicked {
|
||||
fun onDurationValueChanged(duration: Long)
|
||||
}
|
||||
|
|
@ -1,137 +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.main.chat.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.EventLog
|
||||
|
||||
class EventData(private val eventLog: EventLog) : GenericContactData(
|
||||
if (eventLog.type == EventLog.Type.ConferenceSecurityEvent) {
|
||||
eventLog.securityEventFaultyDeviceAddress!!
|
||||
} else {
|
||||
if (eventLog.participantAddress == null) {
|
||||
eventLog.peerAddress!!
|
||||
} else {
|
||||
eventLog.participantAddress!!
|
||||
}
|
||||
}
|
||||
) {
|
||||
val text = MutableLiveData<String>()
|
||||
|
||||
val isSecurity: Boolean by lazy {
|
||||
when (eventLog.type) {
|
||||
EventLog.Type.ConferenceSecurityEvent -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
val isGroupLeft: Boolean by lazy {
|
||||
when (eventLog.type) {
|
||||
EventLog.Type.ConferenceTerminated -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
updateEventText()
|
||||
}
|
||||
|
||||
private fun getName(): String {
|
||||
return contact.value?.name ?: displayName.value ?: ""
|
||||
}
|
||||
|
||||
private fun updateEventText() {
|
||||
val context: Context = coreContext.context
|
||||
|
||||
text.value = when (eventLog.type) {
|
||||
EventLog.Type.ConferenceCreated -> context.getString(
|
||||
R.string.chat_event_conference_created
|
||||
)
|
||||
EventLog.Type.ConferenceTerminated -> context.getString(
|
||||
R.string.chat_event_conference_destroyed
|
||||
)
|
||||
EventLog.Type.ConferenceParticipantAdded -> context.getString(
|
||||
R.string.chat_event_participant_added
|
||||
).format(getName())
|
||||
EventLog.Type.ConferenceParticipantRemoved -> context.getString(
|
||||
R.string.chat_event_participant_removed
|
||||
).format(getName())
|
||||
EventLog.Type.ConferenceSubjectChanged -> context.getString(
|
||||
R.string.chat_event_subject_changed
|
||||
).format(eventLog.subject)
|
||||
EventLog.Type.ConferenceParticipantSetAdmin -> context.getString(
|
||||
R.string.chat_event_admin_set
|
||||
).format(getName())
|
||||
EventLog.Type.ConferenceParticipantUnsetAdmin -> context.getString(
|
||||
R.string.chat_event_admin_unset
|
||||
).format(getName())
|
||||
EventLog.Type.ConferenceParticipantDeviceAdded -> context.getString(
|
||||
R.string.chat_event_device_added
|
||||
).format(getName())
|
||||
EventLog.Type.ConferenceParticipantDeviceRemoved -> context.getString(
|
||||
R.string.chat_event_device_removed
|
||||
).format(getName())
|
||||
EventLog.Type.ConferenceSecurityEvent -> {
|
||||
val name = getName()
|
||||
when (eventLog.securityEventType) {
|
||||
EventLog.SecurityEventType.EncryptionIdentityKeyChanged -> context.getString(
|
||||
R.string.chat_security_event_lime_identity_key_changed
|
||||
).format(name)
|
||||
EventLog.SecurityEventType.ManInTheMiddleDetected -> context.getString(
|
||||
R.string.chat_security_event_man_in_the_middle_detected
|
||||
).format(name)
|
||||
EventLog.SecurityEventType.SecurityLevelDowngraded -> context.getString(
|
||||
R.string.chat_security_event_security_level_downgraded
|
||||
).format(name)
|
||||
EventLog.SecurityEventType.ParticipantMaxDeviceCountExceeded -> context.getString(
|
||||
R.string.chat_security_event_participant_max_count_exceeded
|
||||
).format(name)
|
||||
else -> "Unexpected security event for $name: ${eventLog.securityEventType}"
|
||||
}
|
||||
}
|
||||
EventLog.Type.ConferenceEphemeralMessageDisabled -> context.getString(
|
||||
R.string.chat_event_ephemeral_disabled
|
||||
)
|
||||
EventLog.Type.ConferenceEphemeralMessageEnabled -> context.getString(
|
||||
R.string.chat_event_ephemeral_enabled
|
||||
).format(formatEphemeralExpiration(context, eventLog.ephemeralMessageLifetime))
|
||||
EventLog.Type.ConferenceEphemeralMessageLifetimeChanged -> context.getString(
|
||||
R.string.chat_event_ephemeral_lifetime_changed
|
||||
).format(formatEphemeralExpiration(context, eventLog.ephemeralMessageLifetime))
|
||||
else -> "Unexpected event: ${eventLog.type}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatEphemeralExpiration(context: Context, duration: Long): String {
|
||||
return when (duration) {
|
||||
0L -> context.getString(R.string.chat_room_ephemeral_message_disabled)
|
||||
60L -> context.getString(R.string.chat_room_ephemeral_message_one_minute)
|
||||
3600L -> context.getString(R.string.chat_room_ephemeral_message_one_hour)
|
||||
86400L -> context.getString(R.string.chat_room_ephemeral_message_one_day)
|
||||
259200L -> context.getString(R.string.chat_room_ephemeral_message_three_days)
|
||||
604800L -> context.getString(R.string.chat_room_ephemeral_message_one_week)
|
||||
else -> "Unexpected duration"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* 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.chat.data
|
||||
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.EventLog
|
||||
|
||||
class EventLogData(val eventLog: EventLog) {
|
||||
val type: EventLog.Type = eventLog.type
|
||||
|
||||
val notifyId = eventLog.notifyId
|
||||
|
||||
val data: GenericContactData = if (type == EventLog.Type.ConferenceChatMessage) {
|
||||
ChatMessageData(eventLog.chatMessage!!)
|
||||
} else {
|
||||
EventData(eventLog)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
data.destroy()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,73 +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.main.chat.data
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.GroupChatRoomMember
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class GroupInfoParticipantData(val participant: GroupChatRoomMember) : GenericContactData(
|
||||
participant.address
|
||||
) {
|
||||
val sipUri: String get() = LinphoneUtils.getDisplayableAddress(participant.address)
|
||||
|
||||
val isAdmin = MutableLiveData<Boolean>()
|
||||
|
||||
val showAdminControls = MutableLiveData<Boolean>()
|
||||
|
||||
// A participant not yet added to a group can't be set admin at the same time it's added
|
||||
val canBeSetAdmin = MutableLiveData<Boolean>()
|
||||
|
||||
val securityLevelIcon: Int by lazy {
|
||||
when (participant.securityLevel) {
|
||||
ChatRoom.SecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoom.SecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
}
|
||||
|
||||
val securityLevelContentDescription: Int by lazy {
|
||||
when (participant.securityLevel) {
|
||||
ChatRoom.SecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoom.SecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
securityLevel.value = participant.securityLevel
|
||||
isAdmin.value = participant.isAdmin
|
||||
showAdminControls.value = false
|
||||
canBeSetAdmin.value = participant.canBeSetAdmin
|
||||
}
|
||||
|
||||
fun setAdmin() {
|
||||
isAdmin.value = true
|
||||
participant.isAdmin = true
|
||||
}
|
||||
|
||||
fun unSetAdmin() {
|
||||
isAdmin.value = false
|
||||
participant.isAdmin = false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +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.main.chat.data
|
||||
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.ParticipantImdnState
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ImdnParticipantData(val imdnState: ParticipantImdnState) : GenericContactData(
|
||||
imdnState.participant.address
|
||||
) {
|
||||
val sipUri: String = imdnState.participant.address.asStringUriOnly()
|
||||
|
||||
val time: String = TimestampUtils.toString(imdnState.stateChangeTime)
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
/*
|
||||
* 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.main.chat.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.data.ChatMessageReactionsListData
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatMessageReactionsListDialogBinding
|
||||
import org.linphone.utils.AppUtils
|
||||
|
||||
class ChatMessageReactionsListDialogFragment() : BottomSheetDialogFragment() {
|
||||
companion object {
|
||||
const val TAG = "ChatMessageReactionsListDialogFragment"
|
||||
}
|
||||
|
||||
private lateinit var binding: ChatMessageReactionsListDialogBinding
|
||||
|
||||
private lateinit var data: ChatMessageReactionsListData
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = ChatMessageReactionsListDialogBinding.inflate(layoutInflater)
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
if (::data.isInitialized) {
|
||||
binding.data = data
|
||||
|
||||
data.reactions.observe(viewLifecycleOwner) {
|
||||
binding.tabs.removeAllTabs()
|
||||
binding.tabs.addTab(
|
||||
binding.tabs.newTab().setText(
|
||||
AppUtils.getStringWithPlural(
|
||||
R.plurals.chat_message_reactions_count,
|
||||
it.orEmpty().size
|
||||
)
|
||||
).setId(0)
|
||||
)
|
||||
|
||||
var index = 1
|
||||
data.reactionsMap.forEach { (key, value) ->
|
||||
binding.tabs.addTab(
|
||||
binding.tabs.newTab().setText("$key $value").setId(index).setTag(key)
|
||||
)
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.w("$TAG View created but no message has been set, dismissing...")
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
if (::data.isInitialized) {
|
||||
if (tab.id == 0) {
|
||||
data.updateFilteredReactions("")
|
||||
} else {
|
||||
data.updateFilteredReactions(tab.tag.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {
|
||||
}
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
||||
}
|
||||
})
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
fun setMessage(chatMessage: ChatMessage) {
|
||||
data = ChatMessageReactionsListData(chatMessage)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (::data.isInitialized) {
|
||||
data.onDestroy()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,193 +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.main.chat.fragments
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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.activities.main.chat.viewmodels.ChatRoomCreationViewModel
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.activities.navigateToChatRoom
|
||||
import org.linphone.activities.navigateToGroupInfo
|
||||
import org.linphone.contact.ContactsSelectionAdapter
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomCreationFragmentBinding
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class ChatRoomCreationFragment : SecureFragment<ChatRoomCreationFragmentBinding>() {
|
||||
private lateinit var viewModel: ChatRoomCreationViewModel
|
||||
private lateinit var adapter: ContactsSelectionAdapter
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.chat_room_creation_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false
|
||||
|
||||
val createGroup = arguments?.getBoolean("createGroup") ?: false
|
||||
|
||||
viewModel = ViewModelProvider(this)[ChatRoomCreationViewModel::class.java]
|
||||
viewModel.createGroupChat.value = createGroup
|
||||
|
||||
viewModel.isEncrypted.value = sharedViewModel.createEncryptedChatRoom
|
||||
|
||||
binding.viewModel = viewModel
|
||||
|
||||
adapter = ContactsSelectionAdapter(viewLifecycleOwner)
|
||||
adapter.setGroupChatCapabilityRequired(viewModel.createGroupChat.value == true)
|
||||
adapter.setLimeCapabilityRequired(viewModel.isEncrypted.value == true)
|
||||
binding.contactsList.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.contactsList.layoutManager = layoutManager
|
||||
|
||||
// Divider between items
|
||||
binding.contactsList.addItemDecoration(
|
||||
AppUtils.getDividerDecoration(requireContext(), layoutManager)
|
||||
)
|
||||
|
||||
binding.back.visibility = if ((requireActivity() as GenericActivity).isTablet()) View.INVISIBLE else View.VISIBLE
|
||||
|
||||
binding.setAllContactsToggleClickListener {
|
||||
viewModel.sipContactsSelected.value = false
|
||||
}
|
||||
|
||||
binding.setSipContactsToggleClickListener {
|
||||
viewModel.sipContactsSelected.value = true
|
||||
}
|
||||
|
||||
viewModel.contactsList.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.submitList(it)
|
||||
}
|
||||
|
||||
viewModel.isEncrypted.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.setLimeCapabilityRequired(it)
|
||||
}
|
||||
|
||||
viewModel.sipContactsSelected.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
viewModel.applyFilter()
|
||||
}
|
||||
|
||||
viewModel.selectedAddresses.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.updateSelectedAddresses(it)
|
||||
}
|
||||
|
||||
viewModel.chatRoomCreatedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { chatRoom ->
|
||||
sharedViewModel.selectedChatRoom.value = chatRoom
|
||||
navigateToChatRoom(AppUtils.createBundleWithSharedTextAndFiles(sharedViewModel))
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.filter.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
viewModel.applyFilter()
|
||||
}
|
||||
|
||||
adapter.selectedContact.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { searchResult ->
|
||||
if (createGroup) {
|
||||
viewModel.toggleSelectionForSearchResult(searchResult)
|
||||
} else {
|
||||
viewModel.createOneToOneChat(searchResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addParticipantsFromSharedViewModel()
|
||||
|
||||
// Next button is only used to go to group chat info fragment
|
||||
binding.setNextClickListener {
|
||||
sharedViewModel.createEncryptedChatRoom = viewModel.isEncrypted.value == true
|
||||
sharedViewModel.chatRoomParticipants.value = viewModel.selectedAddresses.value
|
||||
navigateToGroupInfo()
|
||||
}
|
||||
|
||||
viewModel.onMessageToNotifyEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { messageResourceId ->
|
||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||
}
|
||||
}
|
||||
|
||||
if (corePreferences.enableNativeAddressBookIntegration) {
|
||||
if (!PermissionHelper.get().hasReadContactsPermission()) {
|
||||
Log.i("[Chat Room Creation] Asking for READ_CONTACTS permission")
|
||||
requestPermissions(arrayOf(android.Manifest.permission.READ_CONTACTS), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == 0) {
|
||||
val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
if (granted) {
|
||||
Log.i("[Chat Room Creation] READ_CONTACTS permission granted")
|
||||
coreContext.fetchContacts()
|
||||
} else {
|
||||
Log.w("[Chat Room Creation] READ_CONTACTS permission denied")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
viewModel.secureChatAvailable.value = LinphoneUtils.isEndToEndEncryptedChatAvailable()
|
||||
}
|
||||
|
||||
private fun addParticipantsFromSharedViewModel() {
|
||||
val participants = sharedViewModel.chatRoomParticipants.value
|
||||
if (participants != null && participants.size > 0) {
|
||||
viewModel.selectedAddresses.value = participants
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,64 +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.main.chat.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.viewmodels.DevicesListViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.DevicesListViewModelFactory
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomDevicesFragmentBinding
|
||||
|
||||
class DevicesFragment : SecureFragment<ChatRoomDevicesFragmentBinding>() {
|
||||
private lateinit var listViewModel: DevicesListViewModel
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.chat_room_devices_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
if (chatRoom == null) {
|
||||
Log.e("[Devices] Chat room is null, aborting!")
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
|
||||
isSecure = chatRoom.currentParams.isEncryptionEnabled
|
||||
|
||||
listViewModel = ViewModelProvider(
|
||||
this,
|
||||
DevicesListViewModelFactory(chatRoom)
|
||||
)[DevicesListViewModel::class.java]
|
||||
binding.viewModel = listViewModel
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
listViewModel.updateParticipants()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +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.main.chat.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.viewmodels.EphemeralViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.EphemeralViewModelFactory
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomEphemeralFragmentBinding
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class EphemeralFragment : SecureFragment<ChatRoomEphemeralFragmentBinding>() {
|
||||
private lateinit var viewModel: EphemeralViewModel
|
||||
|
||||
override fun getLayoutId(): Int {
|
||||
return R.layout.chat_room_ephemeral_fragment
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
isSecure = true
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
if (chatRoom == null) {
|
||||
Log.e("[Ephemeral] Chat room is null, aborting!")
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
EphemeralViewModelFactory(chatRoom)
|
||||
)[EphemeralViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setValidClickListener {
|
||||
viewModel.updateChatRoomEphemeralDuration()
|
||||
sharedViewModel.refreshChatRoomInListEvent.value = Event(true)
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,238 +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.main.chat.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.chat.GroupChatRoomMember
|
||||
import org.linphone.activities.main.chat.adapters.GroupInfoParticipantsAdapter
|
||||
import org.linphone.activities.main.chat.data.GroupInfoParticipantData
|
||||
import org.linphone.activities.main.chat.viewmodels.GroupInfoViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.GroupInfoViewModelFactory
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.activities.navigateToChatRoom
|
||||
import org.linphone.activities.navigateToChatRoomCreation
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.databinding.ChatRoomGroupInfoFragmentBinding
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.DialogUtils
|
||||
|
||||
class GroupInfoFragment : SecureFragment<ChatRoomGroupInfoFragmentBinding>() {
|
||||
private lateinit var viewModel: GroupInfoViewModel
|
||||
private lateinit var adapter: GroupInfoParticipantsAdapter
|
||||
private var meAdminStatusChangedDialog: Dialog? = null
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.chat_room_group_info_fragment
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
val chatRoom: ChatRoom? = sharedViewModel.selectedGroupChatRoom.value
|
||||
isSecure = chatRoom?.currentParams?.isEncryptionEnabled ?: false
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
GroupInfoViewModelFactory(chatRoom)
|
||||
)[GroupInfoViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.isEncrypted.value = sharedViewModel.createEncryptedChatRoom
|
||||
|
||||
adapter = GroupInfoParticipantsAdapter(
|
||||
viewLifecycleOwner,
|
||||
chatRoom?.hasCapability(ChatRoom.Capabilities.Encrypted.toInt()) ?: (viewModel.isEncrypted.value == true)
|
||||
)
|
||||
binding.participants.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.participants.layoutManager = layoutManager
|
||||
|
||||
// Divider between items
|
||||
binding.participants.addItemDecoration(
|
||||
AppUtils.getDividerDecoration(requireContext(), layoutManager)
|
||||
)
|
||||
|
||||
viewModel.participants.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.submitList(it)
|
||||
}
|
||||
|
||||
viewModel.isMeAdmin.observe(
|
||||
viewLifecycleOwner
|
||||
) { isMeAdmin ->
|
||||
adapter.showAdminControls(isMeAdmin && chatRoom != null)
|
||||
}
|
||||
|
||||
viewModel.meAdminChangedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { isMeAdmin ->
|
||||
showMeAdminStateChanged(isMeAdmin)
|
||||
}
|
||||
}
|
||||
|
||||
adapter.participantRemovedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { participant ->
|
||||
viewModel.removeParticipant(participant)
|
||||
}
|
||||
}
|
||||
|
||||
addParticipantsFromSharedViewModel()
|
||||
|
||||
viewModel.createdChatRoomEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { chatRoom ->
|
||||
goToChatRoom(chatRoom, true)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.updatedChatRoomEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { chatRoom ->
|
||||
goToChatRoom(chatRoom, false)
|
||||
}
|
||||
}
|
||||
|
||||
binding.setNextClickListener {
|
||||
if (viewModel.chatRoom != null) {
|
||||
viewModel.updateRoom()
|
||||
} else {
|
||||
viewModel.createChatRoom()
|
||||
}
|
||||
}
|
||||
|
||||
binding.setParticipantsClickListener {
|
||||
sharedViewModel.createEncryptedChatRoom = corePreferences.forceEndToEndEncryptedChat || viewModel.isEncrypted.value == true
|
||||
|
||||
val list = arrayListOf<Address>()
|
||||
for (participant in viewModel.participants.value.orEmpty()) {
|
||||
list.add(participant.participant.address)
|
||||
}
|
||||
sharedViewModel.chatRoomParticipants.value = list
|
||||
sharedViewModel.chatRoomSubject = viewModel.subject.value.orEmpty()
|
||||
|
||||
val args = Bundle()
|
||||
args.putBoolean("createGroup", true)
|
||||
navigateToChatRoomCreation(args)
|
||||
}
|
||||
|
||||
binding.setLeaveClickListener {
|
||||
val dialogViewModel = DialogViewModel(
|
||||
getString(R.string.chat_room_group_info_leave_dialog_message)
|
||||
)
|
||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||
|
||||
dialogViewModel.showDeleteButton(
|
||||
{
|
||||
viewModel.leaveGroup()
|
||||
dialog.dismiss()
|
||||
},
|
||||
getString(R.string.chat_room_group_info_leave_dialog_button)
|
||||
)
|
||||
|
||||
dialogViewModel.showCancelButton {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
viewModel.onMessageToNotifyEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { messageResourceId ->
|
||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addParticipantsFromSharedViewModel() {
|
||||
val participants = sharedViewModel.chatRoomParticipants.value
|
||||
if (participants != null && participants.size > 0) {
|
||||
val list = arrayListOf<GroupInfoParticipantData>()
|
||||
|
||||
for (address in participants) {
|
||||
val exists = viewModel.participants.value?.find {
|
||||
it.participant.address.weakEqual(address)
|
||||
}
|
||||
|
||||
if (exists != null) {
|
||||
list.add(exists)
|
||||
} else {
|
||||
list.add(
|
||||
GroupInfoParticipantData(
|
||||
GroupChatRoomMember(
|
||||
address,
|
||||
false,
|
||||
hasLimeX3DHCapability = viewModel.isEncrypted.value == true
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.participants.value = list
|
||||
}
|
||||
|
||||
if (sharedViewModel.chatRoomSubject.isNotEmpty()) {
|
||||
viewModel.subject.value = sharedViewModel.chatRoomSubject
|
||||
sharedViewModel.chatRoomSubject = ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMeAdminStateChanged(isMeAdmin: Boolean) {
|
||||
meAdminStatusChangedDialog?.dismiss()
|
||||
|
||||
val message = if (isMeAdmin) {
|
||||
getString(R.string.chat_room_group_info_you_are_now_admin)
|
||||
} else {
|
||||
getString(R.string.chat_room_group_info_you_are_no_longer_admin)
|
||||
}
|
||||
val dialogViewModel = DialogViewModel(message)
|
||||
val dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||
|
||||
dialogViewModel.showOkButton({
|
||||
dialog.dismiss()
|
||||
})
|
||||
|
||||
dialog.show()
|
||||
meAdminStatusChangedDialog = dialog
|
||||
}
|
||||
|
||||
private fun goToChatRoom(chatRoom: ChatRoom, created: Boolean) {
|
||||
sharedViewModel.selectedChatRoom.value = chatRoom
|
||||
navigateToChatRoom(AppUtils.createBundleWithSharedTextAndFiles(sharedViewModel), created)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,101 +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.main.chat.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.adapters.ImdnAdapter
|
||||
import org.linphone.activities.main.chat.viewmodels.ImdnViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.ImdnViewModelFactory
|
||||
import org.linphone.activities.main.fragments.SecureFragment
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomImdnFragmentBinding
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.RecyclerViewHeaderDecoration
|
||||
|
||||
class ImdnFragment : SecureFragment<ChatRoomImdnFragmentBinding>() {
|
||||
private lateinit var viewModel: ImdnViewModel
|
||||
private lateinit var adapter: ImdnAdapter
|
||||
|
||||
override fun getLayoutId(): Int {
|
||||
return R.layout.chat_room_imdn_fragment
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
if (chatRoom == null) {
|
||||
Log.e("[IMDN] Chat room is null, aborting!")
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
|
||||
isSecure = chatRoom.currentParams.isEncryptionEnabled
|
||||
|
||||
if (arguments != null) {
|
||||
val messageId = arguments?.getString("MessageId")
|
||||
val message = if (messageId != null) chatRoom.findMessage(messageId) else null
|
||||
if (message != null) {
|
||||
Log.i("[IMDN] Found message $message with id $messageId")
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
ImdnViewModelFactory(message)
|
||||
)[ImdnViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
} else {
|
||||
Log.e("[IMDN] Couldn't find message with id $messageId in chat room $chatRoom")
|
||||
findNavController().popBackStack()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
Log.e("[IMDN] Couldn't find message id in intent arguments")
|
||||
findNavController().popBackStack()
|
||||
return
|
||||
}
|
||||
|
||||
adapter = ImdnAdapter(viewLifecycleOwner)
|
||||
binding.participantsList.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.participantsList.layoutManager = layoutManager
|
||||
|
||||
// Divider between items
|
||||
binding.participantsList.addItemDecoration(
|
||||
AppUtils.getDividerDecoration(requireContext(), layoutManager)
|
||||
)
|
||||
|
||||
// Displays state header
|
||||
val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter)
|
||||
binding.participantsList.addItemDecoration(headerItemDecoration)
|
||||
|
||||
viewModel.participants.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
adapter.submitList(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,412 +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.main.chat.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.slidingpanelayout.widget.SlidingPaneLayout
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
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.clearDisplayedChatRoom
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.chat.adapters.ChatRoomsListAdapter
|
||||
import org.linphone.activities.main.chat.viewmodels.ChatRoomsListViewModel
|
||||
import org.linphone.activities.main.fragments.MasterFragment
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.activities.navigateToChatRoom
|
||||
import org.linphone.activities.navigateToChatRoomCreation
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.Factory
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomMasterFragmentBinding
|
||||
import org.linphone.utils.*
|
||||
|
||||
class MasterChatRoomsFragment : MasterFragment<ChatRoomMasterFragmentBinding, ChatRoomsListAdapter>() {
|
||||
override val dialogConfirmationMessageBeforeRemoval = R.plurals.chat_room_delete_dialog
|
||||
private lateinit var listViewModel: ChatRoomsListViewModel
|
||||
|
||||
private val observer = object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onChanged() {
|
||||
scrollToTop()
|
||||
}
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
if (positionStart == 0 && itemCount == 1) {
|
||||
scrollToTop()
|
||||
}
|
||||
}
|
||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||
scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLayoutId(): Int = R.layout.chat_room_master_fragment
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding.chatList.adapter = null
|
||||
adapter.unregisterAdapterDataObserver(observer)
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
useMaterialSharedAxisXForwardAnimation = false
|
||||
if (corePreferences.enableAnimations) {
|
||||
val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
|
||||
val axis = if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y
|
||||
enterTransition = MaterialSharedAxis(axis, true)
|
||||
reenterTransition = MaterialSharedAxis(axis, true)
|
||||
returnTransition = MaterialSharedAxis(axis, false)
|
||||
exitTransition = MaterialSharedAxis(axis, false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
isSecure = true
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
listViewModel = requireActivity().run {
|
||||
ViewModelProvider(this)[ChatRoomsListViewModel::class.java]
|
||||
}
|
||||
binding.viewModel = listViewModel
|
||||
|
||||
/* Shared view model & sliding pane related */
|
||||
|
||||
setUpSlidingPane(binding.slidingPane)
|
||||
|
||||
binding.slidingPane.addPanelSlideListener(object : SlidingPaneLayout.PanelSlideListener {
|
||||
override fun onPanelSlide(panel: View, slideOffset: Float) { }
|
||||
|
||||
override fun onPanelOpened(panel: View) { }
|
||||
|
||||
override fun onPanelClosed(panel: View) {
|
||||
// Conversation isn't visible anymore, any new message received in it will trigger a notification
|
||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
||||
}
|
||||
})
|
||||
|
||||
sharedViewModel.layoutChangedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable
|
||||
if (binding.slidingPane.isSlideable) {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment
|
||||
if (navHostFragment.navController.currentDestination?.id == R.id.emptyChatFragment) {
|
||||
Log.i(
|
||||
"[Chat] Foldable device has been folded, closing side pane with empty fragment"
|
||||
)
|
||||
binding.slidingPane.closePane()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sharedViewModel.refreshChatRoomInListEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume {
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
if (chatRoom != null) {
|
||||
listViewModel.notifyChatRoomUpdate(chatRoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* End of shared view model & sliding pane related */
|
||||
|
||||
_adapter = ChatRoomsListAdapter(listSelectionViewModel, viewLifecycleOwner)
|
||||
// SubmitList is done on a background thread
|
||||
// We need this adapter data observer to know when to scroll
|
||||
adapter.registerAdapterDataObserver(observer)
|
||||
|
||||
binding.chatList.setHasFixedSize(true)
|
||||
binding.chatList.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.chatList.layoutManager = layoutManager
|
||||
|
||||
// Swipe action
|
||||
val swipeConfiguration = RecyclerViewSwipeConfiguration()
|
||||
val white = ContextCompat.getColor(requireContext(), R.color.white_color)
|
||||
|
||||
swipeConfiguration.rightToLeftAction = RecyclerViewSwipeConfiguration.Action(
|
||||
requireContext().getString(R.string.dialog_delete),
|
||||
white,
|
||||
ContextCompat.getColor(requireContext(), R.color.red_color)
|
||||
)
|
||||
swipeConfiguration.leftToRightAction = RecyclerViewSwipeConfiguration.Action(
|
||||
requireContext().getString(R.string.received_chat_notification_mark_as_read_label),
|
||||
white,
|
||||
ContextCompat.getColor(requireContext(), R.color.imdn_read_color)
|
||||
)
|
||||
val swipeListener = object : RecyclerViewSwipeListener {
|
||||
override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) {
|
||||
val index = viewHolder.bindingAdapterPosition
|
||||
if (index < 0 || index >= adapter.currentList.size) {
|
||||
Log.e("[Chat] Index is out of bound, can't mark chat room as read")
|
||||
} else {
|
||||
val data = adapter.currentList[viewHolder.bindingAdapterPosition]
|
||||
data.markAsRead()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) {
|
||||
val viewModel = DialogViewModel(getString(R.string.chat_room_delete_one_dialog))
|
||||
viewModel.showIcon = true
|
||||
viewModel.iconResource = R.drawable.dialog_delete_icon
|
||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
|
||||
|
||||
val index = viewHolder.bindingAdapterPosition
|
||||
if (index < 0 || index >= adapter.currentList.size) {
|
||||
Log.e("[Chat] Index is out of bound, can't delete chat room")
|
||||
} else {
|
||||
viewModel.showCancelButton {
|
||||
adapter.notifyItemChanged(viewHolder.bindingAdapterPosition)
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
viewModel.showDeleteButton(
|
||||
{
|
||||
val deletedChatRoom =
|
||||
adapter.currentList[index].chatRoom
|
||||
listViewModel.deleteChatRoom(deletedChatRoom)
|
||||
if (!binding.slidingPane.isSlideable &&
|
||||
deletedChatRoom == sharedViewModel.selectedChatRoom.value
|
||||
) {
|
||||
Log.i(
|
||||
"[Chat] Currently displayed chat room has been deleted, removing detail fragment"
|
||||
)
|
||||
clearDisplayedChatRoom()
|
||||
}
|
||||
dialog.dismiss()
|
||||
},
|
||||
getString(R.string.dialog_delete)
|
||||
)
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
RecyclerViewSwipeUtils(
|
||||
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT,
|
||||
swipeConfiguration,
|
||||
swipeListener
|
||||
)
|
||||
.attachToRecyclerView(binding.chatList)
|
||||
|
||||
// Divider between items
|
||||
binding.chatList.addItemDecoration(
|
||||
AppUtils.getDividerDecoration(requireContext(), layoutManager)
|
||||
)
|
||||
|
||||
listViewModel.chatRooms.observe(
|
||||
viewLifecycleOwner
|
||||
) { chatRooms ->
|
||||
adapter.submitList(chatRooms)
|
||||
}
|
||||
|
||||
listViewModel.chatRoomIndexUpdatedEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { index ->
|
||||
adapter.notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
|
||||
adapter.selectedChatRoomEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { chatRoom ->
|
||||
if ((requireActivity() as GenericActivity).isDestructionPending) {
|
||||
Log.w("[Chat] Activity is pending destruction, don't start navigating now!")
|
||||
sharedViewModel.destructionPendingChatRoom = chatRoom
|
||||
} else {
|
||||
if (chatRoom.peerAddress.asStringUriOnly() == coreContext.notificationsManager.currentlyDisplayedChatRoomAddress) {
|
||||
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!")
|
||||
}
|
||||
} else {
|
||||
sharedViewModel.selectedChatRoom.value = chatRoom
|
||||
navigateToChatRoom(
|
||||
AppUtils.createBundleWithSharedTextAndFiles(
|
||||
sharedViewModel
|
||||
)
|
||||
)
|
||||
binding.slidingPane.openPane()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.setEditClickListener {
|
||||
listSelectionViewModel.isEditionEnabled.value = true
|
||||
}
|
||||
|
||||
binding.setCancelForwardClickListener {
|
||||
sharedViewModel.messageToForwardEvent.value?.consume {
|
||||
Log.i("[Chat] Cancelling message forward")
|
||||
}
|
||||
sharedViewModel.isPendingMessageForward.value = false
|
||||
}
|
||||
|
||||
binding.setCancelSharingClickListener {
|
||||
Log.i("[Chat] Cancelling text/files sharing")
|
||||
sharedViewModel.textToShare.value = ""
|
||||
sharedViewModel.filesToShare.value = arrayListOf()
|
||||
listViewModel.fileSharingPending.value = false
|
||||
listViewModel.textSharingPending.value = false
|
||||
}
|
||||
|
||||
binding.setNewOneToOneChatRoomClickListener {
|
||||
sharedViewModel.chatRoomParticipants.value = arrayListOf()
|
||||
navigateToChatRoomCreation(false, binding.slidingPane)
|
||||
}
|
||||
|
||||
binding.setNewGroupChatRoomClickListener {
|
||||
sharedViewModel.selectedGroupChatRoom.value = null
|
||||
sharedViewModel.chatRoomParticipants.value = arrayListOf()
|
||||
navigateToChatRoomCreation(true, binding.slidingPane)
|
||||
}
|
||||
|
||||
val pendingDestructionChatRoom = sharedViewModel.destructionPendingChatRoom
|
||||
if (pendingDestructionChatRoom != null) {
|
||||
Log.w("[Chat] Found pending chat room from before activity was recreated")
|
||||
sharedViewModel.destructionPendingChatRoom = null
|
||||
sharedViewModel.selectedChatRoom.value = pendingDestructionChatRoom
|
||||
navigateToChatRoom(AppUtils.createBundleWithSharedTextAndFiles(sharedViewModel))
|
||||
}
|
||||
|
||||
val localSipUri = arguments?.getString("LocalSipUri")
|
||||
val remoteSipUri = arguments?.getString("RemoteSipUri")
|
||||
if (localSipUri != null && remoteSipUri != null) {
|
||||
Log.i(
|
||||
"[Chat] Found local [$localSipUri] & remote [$remoteSipUri] addresses in arguments"
|
||||
)
|
||||
arguments?.clear()
|
||||
val localAddress = Factory.instance().createAddress(localSipUri)
|
||||
val remoteSipAddress = Factory.instance().createAddress(remoteSipUri)
|
||||
val chatRoom = coreContext.core.searchChatRoom(
|
||||
null,
|
||||
localAddress,
|
||||
remoteSipAddress,
|
||||
arrayOfNulls(0)
|
||||
)
|
||||
if (chatRoom != null) {
|
||||
Log.i("[Chat] Found matching chat room $chatRoom")
|
||||
adapter.selectedChatRoomEvent.value = Event(chatRoom)
|
||||
}
|
||||
} else {
|
||||
sharedViewModel.textToShare.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
if (it.isNotEmpty()) {
|
||||
Log.i("[Chat] Found text to share")
|
||||
listViewModel.textSharingPending.value = true
|
||||
clearDisplayedChatRoom()
|
||||
} else {
|
||||
if (sharedViewModel.filesToShare.value.isNullOrEmpty()) {
|
||||
listViewModel.textSharingPending.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
sharedViewModel.filesToShare.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
if (it.isNotEmpty()) {
|
||||
Log.i("[Chat] Found ${it.size} files to share")
|
||||
listViewModel.fileSharingPending.value = true
|
||||
clearDisplayedChatRoom()
|
||||
} else {
|
||||
if (sharedViewModel.textToShare.value.isNullOrEmpty()) {
|
||||
listViewModel.fileSharingPending.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
sharedViewModel.isPendingMessageForward.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
listViewModel.forwardPending.value = it
|
||||
adapter.forwardPending(it)
|
||||
if (it) {
|
||||
Log.i("[Chat] Found chat message to transfer")
|
||||
}
|
||||
}
|
||||
|
||||
listViewModel.onMessageToNotifyEvent.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
it.consume { messageResourceId ->
|
||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
listViewModel.groupChatAvailable.value = LinphoneUtils.isGroupChatAvailable()
|
||||
}
|
||||
|
||||
override fun deleteItems(indexesOfItemToDelete: ArrayList<Int>) {
|
||||
val list = ArrayList<ChatRoom>()
|
||||
var closeSlidingPane = false
|
||||
for (index in indexesOfItemToDelete) {
|
||||
val chatRoom = adapter.currentList[index].chatRoom
|
||||
list.add(chatRoom)
|
||||
|
||||
if (chatRoom == sharedViewModel.selectedChatRoom.value) {
|
||||
closeSlidingPane = true
|
||||
}
|
||||
}
|
||||
listViewModel.deleteChatRooms(list)
|
||||
|
||||
if (!binding.slidingPane.isSlideable && closeSlidingPane) {
|
||||
Log.i("[Chat] Currently displayed chat room has been deleted, removing detail fragment")
|
||||
clearDisplayedChatRoom()
|
||||
}
|
||||
}
|
||||
|
||||
private fun scrollToTop() {
|
||||
binding.chatList.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
* 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.chat.receivers
|
||||
|
||||
import android.content.ClipData
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import androidx.core.util.component1
|
||||
import androidx.core.util.component2
|
||||
import androidx.core.view.ContentInfoCompat
|
||||
import androidx.core.view.OnReceiveContentListener
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class RichContentReceiver(private val contentReceived: (uri: Uri) -> Unit) : OnReceiveContentListener {
|
||||
companion object {
|
||||
val MIME_TYPES = arrayOf("image/png", "image/gif", "image/jpeg")
|
||||
}
|
||||
|
||||
override fun onReceiveContent(view: View, payload: ContentInfoCompat): ContentInfoCompat? {
|
||||
val (uriContent, remaining) = payload.partition { item -> item.uri != null }
|
||||
if (uriContent != null) {
|
||||
val clip: ClipData = uriContent.clip
|
||||
for (i in 0 until clip.itemCount) {
|
||||
val uri: Uri = clip.getItemAt(i).uri
|
||||
Log.i("[Content Receiver] Found URI: $uri")
|
||||
contentReceived(uri)
|
||||
}
|
||||
}
|
||||
// Return anything that your app didn't handle. This preserves the default platform
|
||||
// behavior for text and anything else that you aren't implementing custom handling for.
|
||||
return remaining
|
||||
}
|
||||
}
|
||||
|
|
@ -1,596 +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.main.chat.viewmodels
|
||||
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.media.AudioFocusRequestCompat
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.data.ChatMessageAttachmentData
|
||||
import org.linphone.activities.main.chat.data.ChatMessageData
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.*
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class ChatMessageSendingViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ChatMessageSendingViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||
var temporaryFileUploadPath: File? = null
|
||||
|
||||
val attachments = MutableLiveData<ArrayList<ChatMessageAttachmentData>>()
|
||||
|
||||
val attachFileEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val attachFilePending = MutableLiveData<Boolean>()
|
||||
|
||||
val sendMessageEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val attachingFileInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val isReadOnly = MutableLiveData<Boolean>()
|
||||
|
||||
var textToSend = MutableLiveData<String>()
|
||||
|
||||
val isPendingAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
var pendingChatMessageToReplyTo = MutableLiveData<ChatMessageData>()
|
||||
|
||||
val requestRecordAudioPermissionEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val messageSentEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val voiceRecordingProgressBarMax = 10000
|
||||
|
||||
val isPendingVoiceRecord = MutableLiveData<Boolean>()
|
||||
|
||||
val isVoiceRecording = MutableLiveData<Boolean>()
|
||||
|
||||
val voiceRecordingDuration = MutableLiveData<Int>()
|
||||
|
||||
val formattedDuration = MutableLiveData<String>()
|
||||
|
||||
val isPlayingVoiceRecording = MutableLiveData<Boolean>()
|
||||
|
||||
val voiceRecordPlayingPosition = MutableLiveData<Int>()
|
||||
|
||||
val imeFlags: Int = if (chatRoom.hasCapability(ChatRoom.Capabilities.Encrypted.toInt())) {
|
||||
// IME_FLAG_NO_PERSONALIZED_LEARNING is only available on Android 8 and newer
|
||||
Compatibility.getImeFlagsForSecureChatRoom()
|
||||
} else {
|
||||
EditorInfo.IME_NULL
|
||||
}
|
||||
|
||||
val isEmojiPickerOpen = MutableLiveData<Boolean>()
|
||||
|
||||
val isEmojiPickerVisible = MutableLiveData<Boolean>()
|
||||
|
||||
val isFileTransferAvailable = MutableLiveData<Boolean>()
|
||||
|
||||
val requestKeyboardHidingEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
private lateinit var recorder: Recorder
|
||||
|
||||
private var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null
|
||||
|
||||
private lateinit var voiceRecordingPlayer: Player
|
||||
private val playerListener = PlayerListener {
|
||||
Log.i("[Chat Message Sending] End of file reached")
|
||||
stopVoiceRecordPlayer()
|
||||
}
|
||||
|
||||
private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||
updateChatRoomReadOnlyState()
|
||||
}
|
||||
|
||||
override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateChatRoomReadOnlyState()
|
||||
}
|
||||
|
||||
override fun onConferenceLeft(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateChatRoomReadOnlyState()
|
||||
}
|
||||
}
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
init {
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
|
||||
attachments.value = arrayListOf()
|
||||
|
||||
attachFileEnabled.value = true
|
||||
sendMessageEnabled.value = false
|
||||
isEmojiPickerOpen.value = false
|
||||
isEmojiPickerVisible.value = corePreferences.showEmojiPickerButton
|
||||
isFileTransferAvailable.value = LinphoneUtils.isFileTransferAvailable()
|
||||
updateChatRoomReadOnlyState()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
pendingChatMessageToReplyTo.value?.destroy()
|
||||
|
||||
for (pendingAttachment in attachments.value.orEmpty()) {
|
||||
removeAttachment(pendingAttachment)
|
||||
}
|
||||
|
||||
if (this::recorder.isInitialized) {
|
||||
if (recorder.state != Recorder.State.Closed) {
|
||||
recorder.close()
|
||||
}
|
||||
}
|
||||
|
||||
if (this::voiceRecordingPlayer.isInitialized) {
|
||||
stopVoiceRecordPlayer()
|
||||
voiceRecordingPlayer.removeListener(playerListener)
|
||||
}
|
||||
|
||||
chatRoom.removeListener(chatRoomListener)
|
||||
scope.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun onTextToSendChanged(value: String) {
|
||||
sendMessageEnabled.value = value.trim().isNotEmpty() || attachments.value?.isNotEmpty() == true || isPendingVoiceRecord.value == true
|
||||
|
||||
val showEmojiPicker = value.isEmpty() || AppUtils.isTextOnlyContainingEmoji(value)
|
||||
isEmojiPickerVisible.value = corePreferences.showEmojiPickerButton && showEmojiPicker
|
||||
|
||||
if (value.isNotEmpty()) {
|
||||
if (attachFileEnabled.value == true && !corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = false
|
||||
}
|
||||
chatRoom.compose()
|
||||
} else {
|
||||
if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = attachments.value?.isEmpty() ?: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addAttachment(path: String) {
|
||||
val list = arrayListOf<ChatMessageAttachmentData>()
|
||||
list.addAll(attachments.value.orEmpty())
|
||||
list.add(
|
||||
ChatMessageAttachmentData(path) {
|
||||
removeAttachment(it)
|
||||
}
|
||||
)
|
||||
attachments.value = list
|
||||
|
||||
sendMessageEnabled.value = textToSend.value.orEmpty().trim().isNotEmpty() || list.isNotEmpty() || isPendingVoiceRecord.value == true
|
||||
if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeAttachment(attachment: ChatMessageAttachmentData) {
|
||||
val list = arrayListOf<ChatMessageAttachmentData>()
|
||||
list.addAll(attachments.value.orEmpty())
|
||||
list.remove(attachment)
|
||||
attachments.value = list
|
||||
|
||||
val pathToDelete = attachment.path
|
||||
Log.i(
|
||||
"[Chat Message Sending] Attachment is being removed, delete local copy [$pathToDelete]"
|
||||
)
|
||||
FileUtils.deleteFile(pathToDelete)
|
||||
|
||||
sendMessageEnabled.value = textToSend.value.orEmpty().trim().isNotEmpty() || list.isNotEmpty() || isPendingVoiceRecord.value == true
|
||||
if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = list.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleEmojiPicker() {
|
||||
isEmojiPickerOpen.value = isEmojiPickerOpen.value == false
|
||||
if (isEmojiPickerOpen.value == true) {
|
||||
requestKeyboardHidingEvent.value = Event(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createChatMessage(): ChatMessage {
|
||||
val pendingMessageToReplyTo = pendingChatMessageToReplyTo.value
|
||||
return if (isPendingAnswer.value == true && pendingMessageToReplyTo != null) {
|
||||
chatRoom.createReplyMessage(pendingMessageToReplyTo.chatMessage)
|
||||
} else {
|
||||
chatRoom.createEmptyMessage()
|
||||
}
|
||||
}
|
||||
|
||||
fun sendMessage() {
|
||||
if (!isPlayerClosed()) {
|
||||
stopVoiceRecordPlayer()
|
||||
}
|
||||
|
||||
if (isVoiceRecording.value == true) {
|
||||
stopVoiceRecorder()
|
||||
}
|
||||
|
||||
val message = createChatMessage()
|
||||
val isBasicChatRoom: Boolean = chatRoom.hasCapability(ChatRoom.Capabilities.Basic.toInt())
|
||||
|
||||
var voiceRecord = false
|
||||
if (isPendingVoiceRecord.value == true && recorder.file != null) {
|
||||
val content = recorder.createContent()
|
||||
if (content != null) {
|
||||
Log.i(
|
||||
"[Chat Message Sending] Voice recording content created, file name is ${content.name} and duration is ${content.fileDuration}"
|
||||
)
|
||||
message.addContent(content)
|
||||
voiceRecord = true
|
||||
} else {
|
||||
Log.e("[Chat Message Sending] Voice recording content couldn't be created!")
|
||||
}
|
||||
|
||||
isPendingVoiceRecord.value = false
|
||||
isVoiceRecording.value = false
|
||||
}
|
||||
|
||||
val toSend = textToSend.value.orEmpty().trim()
|
||||
if (toSend.isNotEmpty()) {
|
||||
if (voiceRecord && isBasicChatRoom) {
|
||||
val textMessage = createChatMessage()
|
||||
textMessage.addUtf8TextContent(toSend)
|
||||
textMessage.send()
|
||||
} else {
|
||||
message.addUtf8TextContent(toSend)
|
||||
}
|
||||
}
|
||||
|
||||
var fileContent = false
|
||||
for (attachment in attachments.value.orEmpty()) {
|
||||
val content = Factory.instance().createContent()
|
||||
|
||||
content.type = when {
|
||||
attachment.isImage -> "image"
|
||||
attachment.isAudio -> "audio"
|
||||
attachment.isVideo -> "video"
|
||||
attachment.isPdf -> "application"
|
||||
else -> "file"
|
||||
}
|
||||
content.subtype = FileUtils.getExtensionFromFileName(attachment.fileName)
|
||||
content.name = attachment.fileName
|
||||
content.filePath = attachment.path // Let the file body handler take care of the upload
|
||||
|
||||
// Do not send file in the same message as the text in a BasicChatRoom
|
||||
// and don't send multiple files in the same message if setting says so
|
||||
if (isBasicChatRoom or (corePreferences.preventMoreThanOneFilePerMessage and (fileContent or voiceRecord))) {
|
||||
val fileMessage = createChatMessage()
|
||||
fileMessage.addFileContent(content)
|
||||
fileMessage.send()
|
||||
} else {
|
||||
message.addFileContent(content)
|
||||
fileContent = true
|
||||
}
|
||||
}
|
||||
|
||||
if (message.contents.isNotEmpty()) {
|
||||
message.send()
|
||||
}
|
||||
|
||||
cancelReply()
|
||||
attachments.value = arrayListOf()
|
||||
textToSend.value = ""
|
||||
|
||||
messageSentEvent.value = Event(true)
|
||||
}
|
||||
|
||||
fun transferMessage(chatMessage: ChatMessage) {
|
||||
val message = chatRoom.createForwardMessage(chatMessage)
|
||||
message.send()
|
||||
}
|
||||
|
||||
fun cancelReply() {
|
||||
pendingChatMessageToReplyTo.value?.destroy()
|
||||
isPendingAnswer.value = false
|
||||
}
|
||||
|
||||
private fun tickerFlowRecording() = flow {
|
||||
while (isVoiceRecording.value == true) {
|
||||
emit(Unit)
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
|
||||
private fun tickerFlowPlaying() = flow {
|
||||
while (isPlayingVoiceRecording.value == true) {
|
||||
emit(Unit)
|
||||
delay(100)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleVoiceRecording() {
|
||||
if (corePreferences.holdToRecordVoiceMessage) {
|
||||
// Disables click listener just in case, touch listener will be used instead
|
||||
return
|
||||
}
|
||||
|
||||
if (!this::recorder.isInitialized) {
|
||||
initVoiceMessageRecorder()
|
||||
}
|
||||
|
||||
if (isVoiceRecording.value == true) {
|
||||
stopVoiceRecording()
|
||||
} else {
|
||||
startVoiceRecording()
|
||||
}
|
||||
}
|
||||
|
||||
fun startVoiceRecording() {
|
||||
if (!PermissionHelper.get().hasRecordAudioPermission()) {
|
||||
requestRecordAudioPermissionEvent.value = Event(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (voiceRecordAudioFocusRequest == null) {
|
||||
voiceRecordAudioFocusRequest = AppUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
|
||||
coreContext.context
|
||||
)
|
||||
}
|
||||
|
||||
when (recorder.state) {
|
||||
Recorder.State.Running -> Log.w("[Chat Message Sending] Recorder is already recording")
|
||||
Recorder.State.Paused -> {
|
||||
Log.w("[Chat Message Sending] Recorder isn't closed, resuming recording")
|
||||
recorder.start()
|
||||
}
|
||||
Recorder.State.Closed -> {
|
||||
val extension = when (recorder.params.fileFormat) {
|
||||
Recorder.FileFormat.Mkv -> "mkv"
|
||||
else -> "wav"
|
||||
}
|
||||
val tempFileName = "voice-recording-${System.currentTimeMillis()}.$extension"
|
||||
val file = FileUtils.getFileStoragePath(tempFileName)
|
||||
Log.w(
|
||||
"[Chat Message Sending] Recorder is closed, starting recording in ${file.absoluteFile}"
|
||||
)
|
||||
recorder.open(file.absolutePath)
|
||||
recorder.start()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
val duration = recorder.duration
|
||||
voiceRecordingDuration.value = duration
|
||||
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(duration) // duration is in ms
|
||||
|
||||
isPendingVoiceRecord.value = true
|
||||
isVoiceRecording.value = true
|
||||
sendMessageEnabled.value = true
|
||||
|
||||
val maxVoiceRecordDuration = corePreferences.voiceRecordingMaxDuration
|
||||
tickerFlowRecording().onEach {
|
||||
withContext(Dispatchers.Main) {
|
||||
val duration = recorder.duration
|
||||
voiceRecordingDuration.value = recorder.duration % voiceRecordingProgressBarMax
|
||||
formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(
|
||||
duration
|
||||
) // duration is in ms
|
||||
|
||||
if (duration >= maxVoiceRecordDuration) {
|
||||
Log.w(
|
||||
"[Chat Message Sending] Max duration for voice recording exceeded (${maxVoiceRecordDuration}ms), stopping."
|
||||
)
|
||||
stopVoiceRecording()
|
||||
}
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
fun cancelVoiceRecording() {
|
||||
if (recorder.state != Recorder.State.Closed) {
|
||||
Log.i("[Chat Message Sending] Closing voice recorder")
|
||||
recorder.close()
|
||||
|
||||
val path = recorder.file
|
||||
if (path != null) {
|
||||
Log.i("[Chat Message Sending] Deleting voice recording file: $path")
|
||||
FileUtils.deleteFile(path)
|
||||
}
|
||||
}
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AppUtils.releaseAudioFocusForVoiceRecordingOrPlayback(coreContext.context, request)
|
||||
voiceRecordAudioFocusRequest = null
|
||||
}
|
||||
|
||||
isPendingVoiceRecord.value = false
|
||||
isVoiceRecording.value = false
|
||||
sendMessageEnabled.value = textToSend.value.orEmpty().trim().isNotEmpty() == true || attachments.value?.isNotEmpty() == true
|
||||
|
||||
if (!isPlayerClosed()) {
|
||||
stopVoiceRecordPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopVoiceRecorder() {
|
||||
if (recorder.state == Recorder.State.Running) {
|
||||
Log.i("[Chat Message Sending] Pausing / closing voice recorder")
|
||||
recorder.pause()
|
||||
recorder.close()
|
||||
voiceRecordingDuration.value = recorder.duration
|
||||
}
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AppUtils.releaseAudioFocusForVoiceRecordingOrPlayback(coreContext.context, request)
|
||||
voiceRecordAudioFocusRequest = null
|
||||
}
|
||||
|
||||
isVoiceRecording.value = false
|
||||
}
|
||||
|
||||
fun stopVoiceRecording() {
|
||||
stopVoiceRecorder()
|
||||
|
||||
if (corePreferences.sendVoiceRecordingRightAway) {
|
||||
Log.i("[Chat Message Sending] Sending voice recording right away")
|
||||
sendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
fun playRecordedMessage() {
|
||||
if (isPlayerClosed()) {
|
||||
Log.w("[Chat Message Sending] Player closed, let's open it first")
|
||||
initVoiceRecordPlayer()
|
||||
}
|
||||
|
||||
if (AppUtils.isMediaVolumeLow(coreContext.context)) {
|
||||
Toast.makeText(
|
||||
coreContext.context,
|
||||
R.string.chat_message_voice_recording_playback_low_volume,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
if (voiceRecordAudioFocusRequest == null) {
|
||||
voiceRecordAudioFocusRequest = AppUtils.acquireAudioFocusForVoiceRecordingOrPlayback(
|
||||
coreContext.context
|
||||
)
|
||||
}
|
||||
|
||||
voiceRecordingPlayer.start()
|
||||
isPlayingVoiceRecording.value = true
|
||||
|
||||
tickerFlowPlaying().onEach {
|
||||
withContext(Dispatchers.Main) {
|
||||
voiceRecordPlayingPosition.value = voiceRecordingPlayer.currentPosition
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
|
||||
fun pauseRecordedMessage() {
|
||||
Log.i("[Chat Message Sending] Pausing voice record")
|
||||
if (!isPlayerClosed()) {
|
||||
voiceRecordingPlayer.pause()
|
||||
}
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AppUtils.releaseAudioFocusForVoiceRecordingOrPlayback(coreContext.context, request)
|
||||
voiceRecordAudioFocusRequest = null
|
||||
}
|
||||
|
||||
isPlayingVoiceRecording.value = false
|
||||
}
|
||||
|
||||
private fun initVoiceMessageRecorder() {
|
||||
Log.i("[Chat Message Sending] Creating recorder for voice message")
|
||||
val recorderParams = coreContext.core.createRecorderParams()
|
||||
if (corePreferences.voiceMessagesFormatMkv) {
|
||||
recorderParams.fileFormat = Recorder.FileFormat.Mkv
|
||||
} else {
|
||||
recorderParams.fileFormat = Recorder.FileFormat.Wav
|
||||
}
|
||||
|
||||
val recordingAudioDevice = AudioRouteUtils.getAudioRecordingDeviceForVoiceMessage()
|
||||
recorderParams.audioDevice = recordingAudioDevice
|
||||
Log.i(
|
||||
"[Chat Message Sending] Using device ${recorderParams.audioDevice?.id} to make the voice message recording"
|
||||
)
|
||||
|
||||
recorder = coreContext.core.createRecorder(recorderParams)
|
||||
}
|
||||
|
||||
private fun initVoiceRecordPlayer() {
|
||||
Log.i("[Chat Message Sending] Creating player for voice record")
|
||||
|
||||
val playbackSoundCard = AudioRouteUtils.getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage()
|
||||
Log.i(
|
||||
"[Chat Message Sending] Using device $playbackSoundCard to make the voice message playback"
|
||||
)
|
||||
|
||||
val localPlayer = coreContext.core.createLocalPlayer(playbackSoundCard, null, null)
|
||||
if (localPlayer != null) {
|
||||
voiceRecordingPlayer = localPlayer
|
||||
} else {
|
||||
Log.e("[Chat Message Sending] Couldn't create local player!")
|
||||
return
|
||||
}
|
||||
voiceRecordingPlayer.addListener(playerListener)
|
||||
|
||||
val path = recorder.file
|
||||
if (path != null) {
|
||||
voiceRecordingPlayer.open(path)
|
||||
// Update recording duration using player value to ensure proper progress bar animation
|
||||
voiceRecordingDuration.value = voiceRecordingPlayer.duration
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopVoiceRecordPlayer() {
|
||||
if (!isPlayerClosed()) {
|
||||
Log.i("[Chat Message Sending] Stopping voice record")
|
||||
voiceRecordingPlayer.pause()
|
||||
voiceRecordingPlayer.seek(0)
|
||||
voiceRecordPlayingPosition.value = 0
|
||||
voiceRecordingPlayer.close()
|
||||
}
|
||||
|
||||
val request = voiceRecordAudioFocusRequest
|
||||
if (request != null) {
|
||||
AppUtils.releaseAudioFocusForVoiceRecordingOrPlayback(coreContext.context, request)
|
||||
voiceRecordAudioFocusRequest = null
|
||||
}
|
||||
|
||||
isPlayingVoiceRecording.value = false
|
||||
}
|
||||
|
||||
private fun isPlayerClosed(): Boolean {
|
||||
return !this::voiceRecordingPlayer.isInitialized || voiceRecordingPlayer.state == Player.State.Closed
|
||||
}
|
||||
|
||||
private fun updateChatRoomReadOnlyState() {
|
||||
isReadOnly.value = chatRoom.isReadOnly || (
|
||||
chatRoom.hasCapability(
|
||||
ChatRoom.Capabilities.Conference.toInt()
|
||||
) && chatRoom.participants.isEmpty()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,270 +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.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
import org.linphone.activities.main.chat.data.EventLogData
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class ChatMessagesListViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ChatMessagesListViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMessagesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||
companion object {
|
||||
private const val MESSAGES_PER_PAGE = 20
|
||||
}
|
||||
|
||||
val events = MutableLiveData<ArrayList<EventLogData>>()
|
||||
|
||||
val messageUpdatedEvent: MutableLiveData<Event<Int>> by lazy {
|
||||
MutableLiveData<Event<Int>>()
|
||||
}
|
||||
|
||||
val requestWriteExternalStoragePermissionEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() {
|
||||
override fun onChatMessagesReceived(chatRoom: ChatRoom, eventLogs: Array<out EventLog>) {
|
||||
for (eventLog in eventLogs) {
|
||||
addChatMessageEventLog(eventLog)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChatMessageSending(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
val position = events.value.orEmpty().size
|
||||
|
||||
if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val chatMessage = eventLog.chatMessage
|
||||
chatMessage ?: return
|
||||
chatMessage.userData = position
|
||||
}
|
||||
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onSecurityEvent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
if (!chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConferenceLeft(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
if (!chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
Log.i(
|
||||
"[Chat Messages] An ephemeral chat message has expired, removing it from event list"
|
||||
)
|
||||
deleteEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onEphemeralEvent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
|
||||
events.value = getEvents()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
events.value.orEmpty().forEach(EventLogData::destroy)
|
||||
chatRoom.removeListener(chatRoomListener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun resendMessage(chatMessage: ChatMessage) {
|
||||
val position: Int = chatMessage.userData as Int
|
||||
chatMessage.send()
|
||||
messageUpdatedEvent.value = Event(position)
|
||||
}
|
||||
|
||||
fun deleteMessage(chatMessage: ChatMessage) {
|
||||
LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage)
|
||||
chatRoom.deleteMessage(chatMessage)
|
||||
|
||||
events.value.orEmpty().forEach(EventLogData::destroy)
|
||||
events.value = getEvents()
|
||||
}
|
||||
|
||||
fun deleteEventLogs(listToDelete: ArrayList<EventLogData>) {
|
||||
for (eventLog in listToDelete) {
|
||||
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog.eventLog)
|
||||
eventLog.eventLog.deleteFromDatabase()
|
||||
}
|
||||
|
||||
events.value.orEmpty().forEach(EventLogData::destroy)
|
||||
events.value = getEvents()
|
||||
}
|
||||
|
||||
fun loadMoreData(totalItemsCount: Int) {
|
||||
Log.i("[Chat Messages] Load more data, current total is $totalItemsCount")
|
||||
val maxSize: Int = chatRoom.historyEventsSize
|
||||
|
||||
if (totalItemsCount < maxSize) {
|
||||
var upperBound: Int = totalItemsCount + MESSAGES_PER_PAGE
|
||||
if (upperBound > maxSize) {
|
||||
upperBound = maxSize
|
||||
}
|
||||
|
||||
val history: Array<EventLog> = chatRoom.getHistoryRangeEvents(
|
||||
totalItemsCount,
|
||||
upperBound
|
||||
)
|
||||
val list = arrayListOf<EventLogData>()
|
||||
for (eventLog in history) {
|
||||
list.add(EventLogData(eventLog))
|
||||
}
|
||||
list.addAll(events.value.orEmpty())
|
||||
events.value = list
|
||||
}
|
||||
}
|
||||
|
||||
private fun addEvent(eventLog: EventLog) {
|
||||
val list = arrayListOf<EventLogData>()
|
||||
list.addAll(events.value.orEmpty())
|
||||
val found = list.find { data -> data.eventLog == eventLog }
|
||||
if (found == null) {
|
||||
list.add(EventLogData(eventLog))
|
||||
}
|
||||
events.value = list
|
||||
}
|
||||
|
||||
private fun getEvents(): ArrayList<EventLogData> {
|
||||
val list = arrayListOf<EventLogData>()
|
||||
val unreadCount = chatRoom.unreadMessagesCount
|
||||
var loadCount = max(MESSAGES_PER_PAGE, unreadCount)
|
||||
Log.i(
|
||||
"[Chat Messages] $unreadCount unread messages in this chat room, loading $loadCount from history"
|
||||
)
|
||||
|
||||
val history = chatRoom.getHistoryEvents(loadCount)
|
||||
var messageCount = 0
|
||||
for (eventLog in history) {
|
||||
list.add(EventLogData(eventLog))
|
||||
if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
messageCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
// Load enough events to have at least all unread messages
|
||||
while (unreadCount > 0 && messageCount < unreadCount) {
|
||||
Log.w(
|
||||
"[Chat Messages] There is only $messageCount messages in the last $loadCount events, loading $MESSAGES_PER_PAGE more"
|
||||
)
|
||||
val moreHistory = chatRoom.getHistoryRangeEvents(
|
||||
loadCount,
|
||||
loadCount + MESSAGES_PER_PAGE
|
||||
)
|
||||
loadCount += MESSAGES_PER_PAGE
|
||||
for (eventLog in moreHistory) {
|
||||
list.add(EventLogData(eventLog))
|
||||
if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
messageCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
private fun deleteEvent(eventLog: EventLog) {
|
||||
val chatMessage = eventLog.chatMessage
|
||||
if (chatMessage != null) {
|
||||
LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage)
|
||||
chatRoom.deleteMessage(chatMessage)
|
||||
}
|
||||
|
||||
events.value.orEmpty().forEach(EventLogData::destroy)
|
||||
events.value = getEvents()
|
||||
}
|
||||
|
||||
private fun addChatMessageEventLog(eventLog: EventLog) {
|
||||
if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val chatMessage = eventLog.chatMessage
|
||||
chatMessage ?: return
|
||||
chatMessage.userData = events.value.orEmpty().size
|
||||
|
||||
val existingEvent = events.value.orEmpty().find { data ->
|
||||
data.eventLog.type == EventLog.Type.ConferenceChatMessage && data.eventLog.chatMessage?.messageId == chatMessage.messageId
|
||||
}
|
||||
if (existingEvent != null) {
|
||||
Log.w(
|
||||
"[Chat Messages] Found already present chat message, don't add it it's probably the result of an auto download or an aggregated message received before but notified after the conversation was displayed"
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!PermissionHelper.get().hasWriteExternalStoragePermission()) {
|
||||
for (content in chatMessage.contents) {
|
||||
if (content.isFileTransfer) {
|
||||
Log.i(
|
||||
"[Chat Messages] Android < 10 detected and WRITE_EXTERNAL_STORAGE permission isn't granted yet"
|
||||
)
|
||||
requestWriteExternalStoragePermissionEvent.value = Event(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addEvent(eventLog)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,155 +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.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.ContactsSelectionViewModel
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ChatRoomCreationViewModel : ContactsSelectionViewModel() {
|
||||
val chatRoomCreatedEvent: MutableLiveData<Event<ChatRoom>> by lazy {
|
||||
MutableLiveData<Event<ChatRoom>>()
|
||||
}
|
||||
|
||||
val createGroupChat = MutableLiveData<Boolean>()
|
||||
|
||||
val isEncrypted = MutableLiveData<Boolean>()
|
||||
|
||||
val waitForChatRoomCreation = MutableLiveData<Boolean>()
|
||||
|
||||
val secureChatAvailable = MutableLiveData<Boolean>()
|
||||
|
||||
val secureChatMandatory: Boolean = corePreferences.forceEndToEndEncryptedChat
|
||||
|
||||
private val listener = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(room: ChatRoom, state: ChatRoom.State) {
|
||||
if (state == ChatRoom.State.Created) {
|
||||
waitForChatRoomCreation.value = false
|
||||
Log.i("[Chat Room Creation] Chat room created")
|
||||
chatRoomCreatedEvent.value = Event(room)
|
||||
} else if (state == ChatRoom.State.CreationFailed) {
|
||||
Log.e("[Chat Room Creation] Group chat room creation has failed !")
|
||||
waitForChatRoomCreation.value = false
|
||||
onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
createGroupChat.value = false
|
||||
isEncrypted.value = secureChatMandatory
|
||||
waitForChatRoomCreation.value = false
|
||||
secureChatAvailable.value = LinphoneUtils.isEndToEndEncryptedChatAvailable()
|
||||
}
|
||||
|
||||
fun updateEncryption(encrypted: Boolean) {
|
||||
if (!encrypted && secureChatMandatory) {
|
||||
Log.w(
|
||||
"[Chat Room Creation] Something tries to force plain text chat room even if secureChatMandatory is enabled!"
|
||||
)
|
||||
return
|
||||
}
|
||||
isEncrypted.value = encrypted
|
||||
}
|
||||
|
||||
fun createOneToOneChat(searchResult: SearchResult) {
|
||||
waitForChatRoomCreation.value = true
|
||||
val defaultAccount = coreContext.core.defaultAccount
|
||||
var room: ChatRoom?
|
||||
|
||||
val address = searchResult.address ?: coreContext.core.interpretUrl(
|
||||
searchResult.phoneNumber ?: "",
|
||||
LinphoneUtils.applyInternationalPrefix()
|
||||
)
|
||||
if (address == null) {
|
||||
Log.e("[Chat Room Creation] Can't get a valid address from search result $searchResult")
|
||||
onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
|
||||
waitForChatRoomCreation.value = false
|
||||
return
|
||||
}
|
||||
|
||||
val encrypted = secureChatMandatory || isEncrypted.value == true
|
||||
val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams()
|
||||
params.backend = ChatRoom.Backend.Basic
|
||||
params.isGroupEnabled = false
|
||||
if (encrypted) {
|
||||
params.isEncryptionEnabled = true
|
||||
params.backend = ChatRoom.Backend.FlexisipChat
|
||||
params.ephemeralMode = if (corePreferences.useEphemeralPerDeviceMode) {
|
||||
ChatRoom.EphemeralMode.DeviceManaged
|
||||
} else {
|
||||
ChatRoom.EphemeralMode.AdminManaged
|
||||
}
|
||||
params.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default
|
||||
Log.i(
|
||||
"[Chat Room Creation] Ephemeral mode is ${params.ephemeralMode}, lifetime is ${params.ephemeralLifetime}"
|
||||
)
|
||||
params.subject = AppUtils.getString(R.string.chat_room_dummy_subject)
|
||||
}
|
||||
|
||||
val participants = arrayOf(address)
|
||||
val localAddress: Address? = defaultAccount?.params?.identityAddress
|
||||
|
||||
room = coreContext.core.searchChatRoom(params, localAddress, null, participants)
|
||||
if (room == null) {
|
||||
Log.w(
|
||||
"[Chat Room Creation] Couldn't find existing 1-1 chat room with remote ${address.asStringUriOnly()}, encryption=$encrypted and local identity ${localAddress?.asStringUriOnly()}"
|
||||
)
|
||||
room = coreContext.core.createChatRoom(params, localAddress, participants)
|
||||
|
||||
if (room != null) {
|
||||
if (encrypted) {
|
||||
val state = room.state
|
||||
if (state == ChatRoom.State.Created) {
|
||||
Log.i("[Chat Room Creation] Found already created chat room, using it")
|
||||
chatRoomCreatedEvent.value = Event(room)
|
||||
waitForChatRoomCreation.value = false
|
||||
} else {
|
||||
Log.i(
|
||||
"[Chat Room Creation] Chat room creation is pending [$state], waiting for Created state"
|
||||
)
|
||||
room.addListener(listener)
|
||||
}
|
||||
} else {
|
||||
chatRoomCreatedEvent.value = Event(room)
|
||||
waitForChatRoomCreation.value = false
|
||||
}
|
||||
} else {
|
||||
Log.e(
|
||||
"[Chat Room Creation] Couldn't create chat room with remote ${address.asStringUriOnly()} and local identity ${localAddress?.asStringUriOnly()}"
|
||||
)
|
||||
waitForChatRoomCreation.value = false
|
||||
}
|
||||
} else {
|
||||
Log.i(
|
||||
"[Chat Room Creation] Found existing 1-1 chat room with remote ${address.asStringUriOnly()}, encryption=$encrypted and local identity ${localAddress?.asStringUriOnly()}"
|
||||
)
|
||||
chatRoomCreatedEvent.value = Event(room)
|
||||
waitForChatRoomCreation.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,447 +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.main.chat.viewmodels
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.view.animation.LinearInterpolator
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.*
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.ContactDataInterface
|
||||
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ChatRoomViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ChatRoomViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterface {
|
||||
override val contact: MutableLiveData<Friend> = MutableLiveData<Friend>()
|
||||
override val displayName: MutableLiveData<String> = MutableLiveData<String>()
|
||||
override val securityLevel: MutableLiveData<ChatRoom.SecurityLevel> = MutableLiveData<ChatRoom.SecurityLevel>()
|
||||
override val showGroupChatAvatar: Boolean
|
||||
get() = conferenceChatRoom && !oneToOneChatRoom
|
||||
override val presenceStatus: MutableLiveData<ConsolidatedPresence> = MutableLiveData<ConsolidatedPresence>()
|
||||
override val coroutineScope: CoroutineScope = viewModelScope
|
||||
|
||||
val subject = MutableLiveData<String>()
|
||||
|
||||
val participants = MutableLiveData<String>()
|
||||
|
||||
val unreadMessagesCount = MutableLiveData<Int>()
|
||||
|
||||
val remoteIsComposing = MutableLiveData<Boolean>()
|
||||
|
||||
val composingList = MutableLiveData<String>()
|
||||
|
||||
val securityLevelIcon = MutableLiveData<Int>()
|
||||
|
||||
val securityLevelContentDescription = MutableLiveData<Int>()
|
||||
|
||||
val lastPresenceInfo = MutableLiveData<String>()
|
||||
|
||||
val ephemeralEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val basicChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoom.Capabilities.Basic.toInt())
|
||||
}
|
||||
|
||||
val oneToOneChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())
|
||||
}
|
||||
|
||||
private val conferenceChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoom.Capabilities.Conference.toInt())
|
||||
}
|
||||
|
||||
val encryptedChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoom.Capabilities.Encrypted.toInt())
|
||||
}
|
||||
|
||||
val ephemeralChatRoom: Boolean by lazy {
|
||||
chatRoom.hasCapability(ChatRoom.Capabilities.Ephemeral.toInt())
|
||||
}
|
||||
|
||||
val meAdmin: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>()
|
||||
}
|
||||
|
||||
val isUserScrollingUp = MutableLiveData<Boolean>()
|
||||
|
||||
var oneParticipantOneDevice: Boolean = false
|
||||
|
||||
var onlyParticipantOnlyDeviceAddress: Address? = null
|
||||
|
||||
val chatUnreadCountTranslateY = MutableLiveData<Float>()
|
||||
|
||||
val groupCallAvailable: Boolean
|
||||
get() = LinphoneUtils.isRemoteConferencingAvailable()
|
||||
|
||||
private var addressToCall: Address? = null
|
||||
|
||||
private val bounceAnimator: ValueAnimator by lazy {
|
||||
ValueAnimator.ofFloat(
|
||||
AppUtils.getDimension(R.dimen.tabs_fragment_unread_count_bounce_offset),
|
||||
0f
|
||||
).apply {
|
||||
addUpdateListener {
|
||||
val value = it.animatedValue as Float
|
||||
chatUnreadCountTranslateY.value = value
|
||||
}
|
||||
interpolator = LinearInterpolator()
|
||||
duration = 250
|
||||
repeatMode = ValueAnimator.REVERSE
|
||||
repeatCount = ValueAnimator.INFINITE
|
||||
}
|
||||
}
|
||||
|
||||
private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() {
|
||||
override fun onContactsUpdated() {
|
||||
Log.d("[Chat Room] Contacts have changed")
|
||||
contactLookup()
|
||||
}
|
||||
}
|
||||
|
||||
private val coreListener: CoreListenerStub = object : CoreListenerStub() {
|
||||
override fun onChatRoomRead(core: Core, room: ChatRoom) {
|
||||
if (room == chatRoom) {
|
||||
updateUnreadMessageCount()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||
Log.i("[Chat Room] $chatRoom state changed: $state")
|
||||
if (state == ChatRoom.State.Created) {
|
||||
contactLookup()
|
||||
updateSecurityIcon()
|
||||
updateParticipants()
|
||||
subject.value = chatRoom.subject
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
subject.value = chatRoom.subject
|
||||
}
|
||||
|
||||
override fun onChatMessagesReceived(chatRoom: ChatRoom, eventLogs: Array<out EventLog>) {
|
||||
updateUnreadMessageCount()
|
||||
}
|
||||
|
||||
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
contactLookup()
|
||||
updateSecurityIcon()
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
contactLookup()
|
||||
updateSecurityIcon()
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onIsComposingReceived(
|
||||
chatRoom: ChatRoom,
|
||||
remoteAddr: Address,
|
||||
isComposing: Boolean
|
||||
) {
|
||||
updateRemotesComposing()
|
||||
}
|
||||
|
||||
override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
contactLookup()
|
||||
updateSecurityIcon()
|
||||
subject.value = chatRoom.subject
|
||||
}
|
||||
|
||||
override fun onSecurityEvent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateSecurityIcon()
|
||||
}
|
||||
|
||||
override fun onParticipantDeviceAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateSecurityIcon()
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantDeviceRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateSecurityIcon()
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onEphemeralEvent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
ephemeralEnabled.value = chatRoom.isEphemeralEnabled
|
||||
}
|
||||
|
||||
override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
meAdmin.value = chatRoom.me?.isAdmin ?: false
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatRoom.core.addListener(coreListener)
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
coreContext.contactsManager.addListener(contactsUpdatedListener)
|
||||
|
||||
updateUnreadMessageCount()
|
||||
|
||||
subject.value = chatRoom.subject
|
||||
updateSecurityIcon()
|
||||
meAdmin.value = chatRoom.me?.isAdmin ?: false
|
||||
ephemeralEnabled.value = chatRoom.isEphemeralEnabled
|
||||
|
||||
contactLookup()
|
||||
updateParticipants()
|
||||
|
||||
updateRemotesComposing()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.contactsManager.removeListener(contactsUpdatedListener)
|
||||
chatRoom.removeListener(chatRoomListener)
|
||||
chatRoom.core.removeListener(coreListener)
|
||||
if (corePreferences.enableAnimations) bounceAnimator.end()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun contactLookup() {
|
||||
presenceStatus.value = ConsolidatedPresence.Offline
|
||||
displayName.value = when {
|
||||
basicChatRoom -> LinphoneUtils.getDisplayName(
|
||||
chatRoom.peerAddress
|
||||
)
|
||||
oneToOneChatRoom -> LinphoneUtils.getDisplayName(
|
||||
chatRoom.participants.firstOrNull()?.address ?: chatRoom.peerAddress
|
||||
)
|
||||
conferenceChatRoom -> chatRoom.subject.orEmpty()
|
||||
else -> chatRoom.peerAddress.asStringUriOnly()
|
||||
}
|
||||
|
||||
if (oneToOneChatRoom) {
|
||||
searchMatchingContact()
|
||||
} else {
|
||||
getParticipantsNames()
|
||||
}
|
||||
}
|
||||
|
||||
fun startCall() {
|
||||
val address = addressToCall ?: if (basicChatRoom) {
|
||||
chatRoom.peerAddress
|
||||
} else {
|
||||
chatRoom.participants.firstOrNull()?.address
|
||||
}
|
||||
if (address != null) {
|
||||
coreContext.startCall(address)
|
||||
} else {
|
||||
Log.e("[Chat Room] Failed to find a SIP address to call!")
|
||||
}
|
||||
}
|
||||
|
||||
fun startGroupCall() {
|
||||
val conferenceScheduler = coreContext.core.createConferenceScheduler()
|
||||
val conferenceInfo = Factory.instance().createConferenceInfo()
|
||||
|
||||
val localAddress = chatRoom.localAddress.clone()
|
||||
localAddress.clean() // Remove GRUU
|
||||
val addresses = Array(chatRoom.participants.size) {
|
||||
index ->
|
||||
chatRoom.participants[index].address
|
||||
}
|
||||
val localAccount = coreContext.core.accountList.find {
|
||||
account ->
|
||||
account.params.identityAddress?.weakEqual(localAddress) ?: false
|
||||
}
|
||||
|
||||
conferenceInfo.organizer = localAddress
|
||||
conferenceInfo.subject = subject.value
|
||||
conferenceInfo.setParticipants(addresses)
|
||||
conferenceScheduler.account = localAccount
|
||||
// Will trigger the conference creation/update automatically
|
||||
conferenceScheduler.info = conferenceInfo
|
||||
}
|
||||
|
||||
fun areNotificationsMuted(): Boolean {
|
||||
return chatRoom.muted
|
||||
}
|
||||
|
||||
fun muteNotifications(mute: Boolean) {
|
||||
chatRoom.muted = mute
|
||||
}
|
||||
|
||||
fun getRemoteAddress(): Address? {
|
||||
return if (basicChatRoom) {
|
||||
chatRoom.peerAddress
|
||||
} else {
|
||||
if (chatRoom.participants.isNotEmpty()) {
|
||||
chatRoom.participants[0].address
|
||||
} else {
|
||||
Log.e(
|
||||
"[Chat Room] ${chatRoom.peerAddress} doesn't have any participant (state ${chatRoom.state})!"
|
||||
)
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchMatchingContact() {
|
||||
val remoteAddress = getRemoteAddress()
|
||||
if (remoteAddress != null) {
|
||||
val friend = coreContext.contactsManager.findContactByAddress(remoteAddress)
|
||||
if (friend != null) {
|
||||
contact.value = friend!!
|
||||
presenceStatus.value = friend.consolidatedPresence
|
||||
computeLastSeenLabel(friend)
|
||||
friend.addListener {
|
||||
presenceStatus.value = it.consolidatedPresence
|
||||
computeLastSeenLabel(friend)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeLastSeenLabel(friend: Friend) {
|
||||
if (friend.consolidatedPresence == ConsolidatedPresence.Online) {
|
||||
lastPresenceInfo.value = AppUtils.getString(R.string.chat_room_presence_online)
|
||||
return
|
||||
} else if (friend.consolidatedPresence == ConsolidatedPresence.DoNotDisturb) {
|
||||
lastPresenceInfo.value = AppUtils.getString(R.string.chat_room_presence_do_not_disturb)
|
||||
return
|
||||
}
|
||||
|
||||
val timestamp = friend.presenceModel?.latestActivityTimestamp ?: -1L
|
||||
lastPresenceInfo.value = if (timestamp != -1L) {
|
||||
when {
|
||||
TimestampUtils.isToday(timestamp) -> {
|
||||
val time = TimestampUtils.timeToString(timestamp, timestampInSecs = true)
|
||||
val text =
|
||||
AppUtils.getString(R.string.chat_room_presence_last_seen_online_today)
|
||||
"$text $time"
|
||||
}
|
||||
|
||||
TimestampUtils.isYesterday(timestamp) -> {
|
||||
val time = TimestampUtils.timeToString(timestamp, timestampInSecs = true)
|
||||
val text = AppUtils.getString(
|
||||
R.string.chat_room_presence_last_seen_online_yesterday
|
||||
)
|
||||
"$text $time"
|
||||
}
|
||||
|
||||
else -> {
|
||||
val date = TimestampUtils.toString(
|
||||
timestamp,
|
||||
onlyDate = true,
|
||||
shortDate = false,
|
||||
hideYear = true
|
||||
)
|
||||
val text = AppUtils.getString(R.string.chat_room_presence_last_seen_online)
|
||||
"$text $date"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
AppUtils.getString(R.string.chat_room_presence_away)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getParticipantsNames() {
|
||||
if (oneToOneChatRoom) return
|
||||
|
||||
var participantsList = ""
|
||||
var index = 0
|
||||
for (participant in chatRoom.participants) {
|
||||
val contact = coreContext.contactsManager.findContactByAddress(participant.address)
|
||||
participantsList += contact?.name ?: LinphoneUtils.getDisplayName(participant.address)
|
||||
index++
|
||||
if (index != chatRoom.nbParticipants) participantsList += ", "
|
||||
}
|
||||
participants.value = participantsList
|
||||
}
|
||||
|
||||
private fun updateSecurityIcon() {
|
||||
val level = chatRoom.securityLevel
|
||||
securityLevel.value = level
|
||||
|
||||
securityLevelIcon.value = when (level) {
|
||||
ChatRoom.SecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoom.SecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
securityLevelContentDescription.value = when (level) {
|
||||
ChatRoom.SecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoom.SecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRemotesComposing() {
|
||||
val isComposing = chatRoom.isRemoteComposing
|
||||
remoteIsComposing.value = isComposing
|
||||
if (!isComposing) return
|
||||
|
||||
var composing = ""
|
||||
for (address in chatRoom.composingAddresses) {
|
||||
val contact = coreContext.contactsManager.findContactByAddress(address)
|
||||
composing += if (composing.isNotEmpty()) ", " else ""
|
||||
composing += contact?.name ?: LinphoneUtils.getDisplayName(address)
|
||||
}
|
||||
composingList.value = AppUtils.getStringWithPlural(
|
||||
R.plurals.chat_room_remote_composing,
|
||||
chatRoom.composingAddresses.size,
|
||||
composing
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateParticipants() {
|
||||
val participants = chatRoom.participants
|
||||
|
||||
oneParticipantOneDevice = oneToOneChatRoom &&
|
||||
chatRoom.me?.devices?.size == 1 &&
|
||||
participants.firstOrNull()?.devices?.size == 1
|
||||
|
||||
addressToCall = if (basicChatRoom) {
|
||||
chatRoom.peerAddress
|
||||
} else {
|
||||
participants.firstOrNull()?.address
|
||||
}
|
||||
|
||||
onlyParticipantOnlyDeviceAddress = participants.firstOrNull()?.devices?.firstOrNull()?.address
|
||||
}
|
||||
|
||||
private fun updateUnreadMessageCount() {
|
||||
val count = chatRoom.unreadMessagesCount
|
||||
unreadMessagesCount.value = count
|
||||
if (count > 0 && corePreferences.enableAnimations) {
|
||||
bounceAnimator.start()
|
||||
} else if (count == 0 && bounceAnimator.isStarted) bounceAnimator.end()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,195 +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.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.data.ChatRoomData
|
||||
import org.linphone.activities.main.viewmodels.MessageNotifierViewModel
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ChatRoomsListViewModel : MessageNotifierViewModel() {
|
||||
val chatRooms = MutableLiveData<ArrayList<ChatRoomData>>()
|
||||
|
||||
val fileSharingPending = MutableLiveData<Boolean>()
|
||||
|
||||
val textSharingPending = MutableLiveData<Boolean>()
|
||||
|
||||
val forwardPending = MutableLiveData<Boolean>()
|
||||
|
||||
val groupChatAvailable = MutableLiveData<Boolean>()
|
||||
|
||||
val chatRoomIndexUpdatedEvent: MutableLiveData<Event<Int>> by lazy {
|
||||
MutableLiveData<Event<Int>>()
|
||||
}
|
||||
|
||||
private val chatRoomListener = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(chatRoom: ChatRoom, newState: ChatRoom.State) {
|
||||
if (newState == ChatRoom.State.Deleted) {
|
||||
Log.i(
|
||||
"[Chat Rooms] Chat room [${LinphoneUtils.getChatRoomId(chatRoom)}] is in Deleted state, removing it from list"
|
||||
)
|
||||
val list = arrayListOf<ChatRoomData>()
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
for (data in chatRooms.value.orEmpty()) {
|
||||
if (data.id != id) {
|
||||
list.add(data)
|
||||
}
|
||||
}
|
||||
chatRooms.value = list
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val listener: CoreListenerStub = object : CoreListenerStub() {
|
||||
override fun onChatRoomStateChanged(core: Core, chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||
if (state == ChatRoom.State.Created) {
|
||||
Log.i(
|
||||
"[Chat Rooms] Chat room [${LinphoneUtils.getChatRoomId(chatRoom)}] is in Created state, adding it to list"
|
||||
)
|
||||
val data = ChatRoomData(chatRoom)
|
||||
val list = arrayListOf<ChatRoomData>()
|
||||
list.add(data)
|
||||
list.addAll(chatRooms.value.orEmpty())
|
||||
chatRooms.value = list
|
||||
} else if (state == ChatRoom.State.TerminationFailed) {
|
||||
Log.e(
|
||||
"[Chat Rooms] Group chat room removal for address ${chatRoom.peerAddress.asStringUriOnly()} has failed !"
|
||||
)
|
||||
onMessageToNotifyEvent.value = Event(R.string.chat_room_removal_failed_snack)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMessageSent(core: Core, chatRoom: ChatRoom, message: ChatMessage) {
|
||||
onChatRoomMessageEvent(chatRoom)
|
||||
}
|
||||
|
||||
override fun onMessagesReceived(
|
||||
core: Core,
|
||||
chatRoom: ChatRoom,
|
||||
messages: Array<out ChatMessage>
|
||||
) {
|
||||
onChatRoomMessageEvent(chatRoom)
|
||||
}
|
||||
|
||||
override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) {
|
||||
notifyChatRoomUpdate(chatRoom)
|
||||
}
|
||||
|
||||
override fun onChatRoomEphemeralMessageDeleted(core: Core, chatRoom: ChatRoom) {
|
||||
notifyChatRoomUpdate(chatRoom)
|
||||
}
|
||||
|
||||
override fun onChatRoomSubjectChanged(core: Core, chatRoom: ChatRoom) {
|
||||
notifyChatRoomUpdate(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
private var chatRoomsToDeleteCount = 0
|
||||
|
||||
init {
|
||||
groupChatAvailable.value = LinphoneUtils.isGroupChatAvailable()
|
||||
updateChatRooms()
|
||||
coreContext.core.addListener(listener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun deleteChatRoom(chatRoom: ChatRoom?) {
|
||||
for (eventLog in chatRoom?.getHistoryMessageEvents(0).orEmpty()) {
|
||||
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog)
|
||||
}
|
||||
|
||||
chatRoomsToDeleteCount = 1
|
||||
if (chatRoom != null) {
|
||||
coreContext.notificationsManager.dismissChatNotification(chatRoom)
|
||||
Compatibility.removeChatRoomShortcut(coreContext.context, chatRoom)
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
coreContext.core.deleteChatRoom(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteChatRooms(chatRooms: ArrayList<ChatRoom>) {
|
||||
chatRoomsToDeleteCount = chatRooms.size
|
||||
for (chatRoom in chatRooms) {
|
||||
for (eventLog in chatRoom.getHistoryMessageEvents(0)) {
|
||||
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog)
|
||||
}
|
||||
|
||||
coreContext.notificationsManager.dismissChatNotification(chatRoom)
|
||||
Compatibility.removeChatRoomShortcut(coreContext.context, chatRoom)
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
chatRoom.core.deleteChatRoom(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateChatRooms() {
|
||||
chatRooms.value.orEmpty().forEach(ChatRoomData::destroy)
|
||||
|
||||
val list = arrayListOf<ChatRoomData>()
|
||||
for (chatRoom in coreContext.core.chatRooms) {
|
||||
list.add(ChatRoomData(chatRoom))
|
||||
}
|
||||
chatRooms.value = list
|
||||
}
|
||||
|
||||
fun notifyChatRoomUpdate(chatRoom: ChatRoom) {
|
||||
val index = findChatRoomIndex(chatRoom)
|
||||
if (index == -1) {
|
||||
updateChatRooms()
|
||||
} else {
|
||||
chatRoomIndexUpdatedEvent.value = Event(index)
|
||||
}
|
||||
}
|
||||
|
||||
private fun reorderChatRooms() {
|
||||
val list = arrayListOf<ChatRoomData>()
|
||||
list.addAll(chatRooms.value.orEmpty())
|
||||
list.sortByDescending { data -> data.chatRoom.lastUpdateTime }
|
||||
chatRooms.value = list
|
||||
}
|
||||
|
||||
private fun findChatRoomIndex(chatRoom: ChatRoom): Int {
|
||||
val id = LinphoneUtils.getChatRoomId(chatRoom)
|
||||
for ((index, data) in chatRooms.value.orEmpty().withIndex()) {
|
||||
if (id == data.id) {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
private fun onChatRoomMessageEvent(chatRoom: ChatRoom) {
|
||||
when (findChatRoomIndex(chatRoom)) {
|
||||
-1 -> updateChatRooms()
|
||||
0 -> chatRoomIndexUpdatedEvent.value = Event(0)
|
||||
else -> reorderChatRooms()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,83 +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.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.activities.main.chat.data.DevicesListGroupData
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.ChatRoomListenerStub
|
||||
import org.linphone.core.EventLog
|
||||
|
||||
class DevicesListViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return DevicesListViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class DevicesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||
val participants = MutableLiveData<ArrayList<DevicesListGroupData>>()
|
||||
|
||||
private val listener = object : ChatRoomListenerStub() {
|
||||
override fun onParticipantDeviceAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantDeviceRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatRoom.addListener(listener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
participants.value.orEmpty().forEach(DevicesListGroupData::destroy)
|
||||
chatRoom.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun updateParticipants() {
|
||||
participants.value.orEmpty().forEach(DevicesListGroupData::destroy)
|
||||
|
||||
val list = arrayListOf<DevicesListGroupData>()
|
||||
val me = chatRoom.me
|
||||
if (me != null) list.add(DevicesListGroupData(me))
|
||||
for (participant in chatRoom.participants) {
|
||||
list.add(DevicesListGroupData(participant))
|
||||
}
|
||||
|
||||
participants.value = list
|
||||
}
|
||||
}
|
||||
|
|
@ -1,136 +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.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.data.DurationItemClicked
|
||||
import org.linphone.activities.main.chat.data.EphemeralDurationData
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class EphemeralViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return EphemeralViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class EphemeralViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||
val durationsList = MutableLiveData<ArrayList<EphemeralDurationData>>()
|
||||
|
||||
var currentSelectedDuration: Long = 0
|
||||
|
||||
private val listener = object : DurationItemClicked {
|
||||
override fun onDurationValueChanged(duration: Long) {
|
||||
currentSelectedDuration = duration
|
||||
computeEphemeralDurationValues()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
Log.i(
|
||||
"[Ephemeral Messages] Current lifetime is ${chatRoom.ephemeralLifetime}, ephemeral enabled? ${chatRoom.isEphemeralEnabled}"
|
||||
)
|
||||
currentSelectedDuration = if (chatRoom.isEphemeralEnabled) chatRoom.ephemeralLifetime else 0
|
||||
computeEphemeralDurationValues()
|
||||
}
|
||||
|
||||
fun updateChatRoomEphemeralDuration() {
|
||||
Log.i("[Ephemeral Messages] Selected value is $currentSelectedDuration")
|
||||
if (currentSelectedDuration > 0) {
|
||||
if (chatRoom.ephemeralLifetime != currentSelectedDuration) {
|
||||
Log.i(
|
||||
"[Ephemeral Messages] Setting new lifetime for ephemeral messages to $currentSelectedDuration"
|
||||
)
|
||||
chatRoom.ephemeralLifetime = currentSelectedDuration
|
||||
} else {
|
||||
Log.i(
|
||||
"[Ephemeral Messages] Configured lifetime for ephemeral messages was already $currentSelectedDuration"
|
||||
)
|
||||
}
|
||||
|
||||
if (!chatRoom.isEphemeralEnabled) {
|
||||
Log.i("[Ephemeral Messages] Ephemeral messages were disabled, enable them")
|
||||
chatRoom.isEphemeralEnabled = true
|
||||
}
|
||||
} else if (chatRoom.isEphemeralEnabled) {
|
||||
Log.i("[Ephemeral Messages] Ephemeral messages were enabled, disable them")
|
||||
chatRoom.isEphemeralEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeEphemeralDurationValues() {
|
||||
val list = arrayListOf<EphemeralDurationData>()
|
||||
list.add(
|
||||
EphemeralDurationData(
|
||||
R.string.chat_room_ephemeral_message_disabled,
|
||||
currentSelectedDuration,
|
||||
0,
|
||||
listener
|
||||
)
|
||||
)
|
||||
list.add(
|
||||
EphemeralDurationData(
|
||||
R.string.chat_room_ephemeral_message_one_minute,
|
||||
currentSelectedDuration,
|
||||
60,
|
||||
listener
|
||||
)
|
||||
)
|
||||
list.add(
|
||||
EphemeralDurationData(
|
||||
R.string.chat_room_ephemeral_message_one_hour,
|
||||
currentSelectedDuration,
|
||||
3600,
|
||||
listener
|
||||
)
|
||||
)
|
||||
list.add(
|
||||
EphemeralDurationData(
|
||||
R.string.chat_room_ephemeral_message_one_day,
|
||||
currentSelectedDuration,
|
||||
86400,
|
||||
listener
|
||||
)
|
||||
)
|
||||
list.add(
|
||||
EphemeralDurationData(
|
||||
R.string.chat_room_ephemeral_message_three_days,
|
||||
currentSelectedDuration,
|
||||
259200,
|
||||
listener
|
||||
)
|
||||
)
|
||||
list.add(
|
||||
EphemeralDurationData(
|
||||
R.string.chat_room_ephemeral_message_one_week,
|
||||
currentSelectedDuration,
|
||||
604800,
|
||||
listener
|
||||
)
|
||||
)
|
||||
durationsList.value = list
|
||||
}
|
||||
}
|
||||
|
|
@ -1,257 +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.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.GroupChatRoomMember
|
||||
import org.linphone.activities.main.chat.data.GroupInfoParticipantData
|
||||
import org.linphone.activities.main.viewmodels.MessageNotifierViewModel
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class GroupInfoViewModelFactory(private val chatRoom: ChatRoom?) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return GroupInfoViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class GroupInfoViewModel(val chatRoom: ChatRoom?) : MessageNotifierViewModel() {
|
||||
val createdChatRoomEvent = MutableLiveData<Event<ChatRoom>>()
|
||||
val updatedChatRoomEvent = MutableLiveData<Event<ChatRoom>>()
|
||||
|
||||
val subject = MutableLiveData<String>()
|
||||
|
||||
val participants = MutableLiveData<ArrayList<GroupInfoParticipantData>>()
|
||||
|
||||
val isEncrypted = MutableLiveData<Boolean>()
|
||||
|
||||
val isMeAdmin = MutableLiveData<Boolean>()
|
||||
|
||||
val canLeaveGroup = MutableLiveData<Boolean>()
|
||||
|
||||
val waitForChatRoomCreation = MutableLiveData<Boolean>()
|
||||
|
||||
val meAdminChangedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
private val listener = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||
if (state == ChatRoom.State.Created) {
|
||||
waitForChatRoomCreation.value = false
|
||||
createdChatRoomEvent.value = Event(chatRoom) // To trigger going to the chat room
|
||||
} else if (state == ChatRoom.State.CreationFailed) {
|
||||
Log.e("[Chat Room Group Info] Group chat room creation has failed !")
|
||||
waitForChatRoomCreation.value = false
|
||||
onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
subject.value = chatRoom.subject
|
||||
}
|
||||
|
||||
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
val admin = chatRoom.me?.isAdmin ?: false
|
||||
if (admin != isMeAdmin.value) {
|
||||
isMeAdmin.value = admin
|
||||
meAdminChangedEvent.value = Event(admin)
|
||||
}
|
||||
updateParticipants()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
subject.value = chatRoom?.subject
|
||||
isMeAdmin.value = chatRoom == null || (chatRoom.me?.isAdmin == true && !chatRoom.isReadOnly)
|
||||
canLeaveGroup.value = chatRoom != null && !chatRoom.isReadOnly
|
||||
isEncrypted.value = corePreferences.forceEndToEndEncryptedChat || chatRoom?.hasCapability(
|
||||
ChatRoom.Capabilities.Encrypted.toInt()
|
||||
) == true
|
||||
|
||||
if (chatRoom != null) updateParticipants()
|
||||
|
||||
chatRoom?.addListener(listener)
|
||||
waitForChatRoomCreation.value = false
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
participants.value.orEmpty().forEach(GroupInfoParticipantData::destroy)
|
||||
chatRoom?.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun createChatRoom() {
|
||||
waitForChatRoomCreation.value = true
|
||||
val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams()
|
||||
params.isEncryptionEnabled = corePreferences.forceEndToEndEncryptedChat || isEncrypted.value == true
|
||||
params.isGroupEnabled = true
|
||||
if (params.isEncryptionEnabled) {
|
||||
params.ephemeralMode = if (corePreferences.useEphemeralPerDeviceMode) {
|
||||
ChatRoom.EphemeralMode.DeviceManaged
|
||||
} else {
|
||||
ChatRoom.EphemeralMode.AdminManaged
|
||||
}
|
||||
}
|
||||
params.ephemeralLifetime = 0 // Make sure ephemeral is disabled by default
|
||||
Log.i(
|
||||
"[Chat Room Group Info] Ephemeral mode is ${params.ephemeralMode}, lifetime is ${params.ephemeralLifetime}"
|
||||
)
|
||||
params.subject = subject.value
|
||||
|
||||
val addresses = arrayOfNulls<Address>(participants.value.orEmpty().size)
|
||||
var index = 0
|
||||
for (participant in participants.value.orEmpty()) {
|
||||
addresses[index] = participant.participant.address
|
||||
Log.i("[Chat Room Group Info] Participant ${participant.sipUri} will be added to group")
|
||||
index += 1
|
||||
}
|
||||
|
||||
val chatRoom: ChatRoom? = coreContext.core.createChatRoom(
|
||||
params,
|
||||
coreContext.core.defaultAccount?.params?.identityAddress,
|
||||
addresses
|
||||
)
|
||||
chatRoom?.addListener(listener)
|
||||
if (chatRoom == null) {
|
||||
Log.e("[Chat Room Group Info] Couldn't create chat room!")
|
||||
waitForChatRoomCreation.value = false
|
||||
onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateRoom() {
|
||||
if (chatRoom != null) {
|
||||
// Subject
|
||||
val newSubject = subject.value.orEmpty()
|
||||
if (newSubject.isNotEmpty() && newSubject != chatRoom.subject) {
|
||||
Log.i("[Chat Room Group Info] Subject changed to $newSubject")
|
||||
chatRoom.subject = newSubject
|
||||
}
|
||||
|
||||
// Removed participants
|
||||
val participantsToRemove = arrayListOf<Participant>()
|
||||
for (participant in chatRoom.participants) {
|
||||
val member = participants.value.orEmpty().find { member ->
|
||||
participant.address.weakEqual(member.participant.address)
|
||||
}
|
||||
if (member == null) {
|
||||
Log.w(
|
||||
"[Chat Room Group Info] Participant ${participant.address.asStringUriOnly()} will be removed from group"
|
||||
)
|
||||
participantsToRemove.add(participant)
|
||||
}
|
||||
}
|
||||
val toRemove = arrayOfNulls<Participant>(participantsToRemove.size)
|
||||
participantsToRemove.toArray(toRemove)
|
||||
chatRoom.removeParticipants(toRemove)
|
||||
|
||||
// Added participants & new admins
|
||||
val participantsToAdd = arrayListOf<Address>()
|
||||
for (member in participants.value.orEmpty()) {
|
||||
val participant = chatRoom.participants.find { participant ->
|
||||
participant.address.weakEqual(member.participant.address)
|
||||
}
|
||||
if (participant != null) {
|
||||
// Participant found, check if admin status needs to be updated
|
||||
if (member.participant.isAdmin != participant.isAdmin) {
|
||||
if (chatRoom.me?.isAdmin == true) {
|
||||
Log.i(
|
||||
"[Chat Room Group Info] Participant ${member.sipUri} will be admin? ${member.isAdmin}"
|
||||
)
|
||||
chatRoom.setParticipantAdminStatus(
|
||||
participant,
|
||||
member.participant.isAdmin
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.i(
|
||||
"[Chat Room Group Info] Participant ${member.sipUri} will be added to group"
|
||||
)
|
||||
participantsToAdd.add(member.participant.address)
|
||||
}
|
||||
}
|
||||
val toAdd = arrayOfNulls<Address>(participantsToAdd.size)
|
||||
participantsToAdd.toArray(toAdd)
|
||||
chatRoom.addParticipants(toAdd)
|
||||
|
||||
// Go back to chat room
|
||||
updatedChatRoomEvent.value = Event(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
fun leaveGroup() {
|
||||
if (chatRoom != null) {
|
||||
Log.w("[Chat Room Group Info] Leaving group")
|
||||
chatRoom.leave()
|
||||
updatedChatRoomEvent.value = Event(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeParticipant(participant: GroupChatRoomMember) {
|
||||
val list = arrayListOf<GroupInfoParticipantData>()
|
||||
for (data in participants.value.orEmpty()) {
|
||||
if (!data.participant.address.weakEqual(participant.address)) {
|
||||
list.add(data)
|
||||
}
|
||||
}
|
||||
participants.value = list
|
||||
}
|
||||
|
||||
private fun updateParticipants() {
|
||||
val list = arrayListOf<GroupInfoParticipantData>()
|
||||
|
||||
if (chatRoom != null) {
|
||||
for (participant in chatRoom.participants) {
|
||||
list.add(
|
||||
GroupInfoParticipantData(
|
||||
GroupChatRoomMember(
|
||||
participant.address,
|
||||
participant.isAdmin,
|
||||
participant.securityLevel,
|
||||
canBeSetAdmin = true
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
participants.value = list
|
||||
}
|
||||
}
|
||||
|
|
@ -1,85 +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.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.activities.main.chat.data.ChatMessageData
|
||||
import org.linphone.activities.main.chat.data.ImdnParticipantData
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.core.ChatMessageListenerStub
|
||||
import org.linphone.core.ParticipantImdnState
|
||||
|
||||
class ImdnViewModelFactory(private val chatMessage: ChatMessage) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ImdnViewModel(chatMessage) as T
|
||||
}
|
||||
}
|
||||
|
||||
class ImdnViewModel(private val chatMessage: ChatMessage) : ViewModel() {
|
||||
val participants = MutableLiveData<ArrayList<ImdnParticipantData>>()
|
||||
|
||||
val chatMessageViewModel = ChatMessageData(chatMessage)
|
||||
|
||||
private val listener = object : ChatMessageListenerStub() {
|
||||
override fun onParticipantImdnStateChanged(
|
||||
message: ChatMessage,
|
||||
state: ParticipantImdnState
|
||||
) {
|
||||
updateParticipantsLists()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatMessage.addListener(listener)
|
||||
updateParticipantsLists()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
participants.value.orEmpty().forEach(ImdnParticipantData::destroy)
|
||||
chatMessage.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
private fun updateParticipantsLists() {
|
||||
val list = arrayListOf<ImdnParticipantData>()
|
||||
|
||||
for (participant in chatMessage.getParticipantsByImdnState(ChatMessage.State.Displayed)) {
|
||||
list.add(ImdnParticipantData(participant))
|
||||
}
|
||||
for (participant in chatMessage.getParticipantsByImdnState(
|
||||
ChatMessage.State.DeliveredToUser
|
||||
)) {
|
||||
list.add(ImdnParticipantData(participant))
|
||||
}
|
||||
for (participant in chatMessage.getParticipantsByImdnState(ChatMessage.State.Delivered)) {
|
||||
list.add(ImdnParticipantData(participant))
|
||||
}
|
||||
for (participant in chatMessage.getParticipantsByImdnState(ChatMessage.State.NotDelivered)) {
|
||||
list.add(ImdnParticipantData(participant))
|
||||
}
|
||||
|
||||
participants.value = list
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +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.main.chat.views
|
||||
|
||||
import android.content.Context
|
||||
import android.text.Layout
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.max
|
||||
import kotlin.math.round
|
||||
|
||||
/**
|
||||
* The purpose of this class is to have a TextView declared with wrap_content as width that won't
|
||||
* fill it's parent if it is multi line.
|
||||
*/
|
||||
class MultiLineWrapContentWidthTextView : AppCompatTextView {
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet?,
|
||||
defStyleAttr: Int
|
||||
) : super(context, attrs, defStyleAttr)
|
||||
|
||||
override fun setText(text: CharSequence?, type: BufferType?) {
|
||||
super.setText(text, type)
|
||||
// Required for PatternClickableSpan
|
||||
movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
||||
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
||||
super.onMeasure(widthSpec, heightSpec)
|
||||
|
||||
if (layout != null && layout.lineCount >= 2) {
|
||||
val maxLineWidth = ceil(getMaxLineWidth(layout)).toInt()
|
||||
if (maxLineWidth < measuredWidth) {
|
||||
super.onMeasure(
|
||||
MeasureSpec.makeMeasureSpec(maxLineWidth, MeasureSpec.getMode(widthSpec)),
|
||||
heightSpec
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMaxLineWidth(layout: Layout): Float {
|
||||
var maxWidth = 0.0f
|
||||
val lines = layout.lineCount
|
||||
for (i in 0 until lines) {
|
||||
maxWidth = max(maxWidth, layout.getLineWidth(i))
|
||||
}
|
||||
return round(maxWidth)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,97 +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.main.chat.views
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.KeyEvent
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.ViewModelStoreOwner
|
||||
import org.linphone.activities.main.chat.receivers.RichContentReceiver
|
||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
/**
|
||||
* Allows for image input inside an EditText, usefull for keyboards with gif support for example.
|
||||
*/
|
||||
class RichEditText : AppCompatEditText {
|
||||
private var controlPressed = false
|
||||
|
||||
private var sendListener: RichEditTextSendListener? = null
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
initReceiveContentListener()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
initReceiveContentListener()
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyleAttr
|
||||
) {
|
||||
initReceiveContentListener()
|
||||
}
|
||||
|
||||
fun setControlEnterListener(listener: RichEditTextSendListener) {
|
||||
sendListener = listener
|
||||
}
|
||||
|
||||
private fun initReceiveContentListener() {
|
||||
ViewCompat.setOnReceiveContentListener(
|
||||
this,
|
||||
RichContentReceiver.MIME_TYPES,
|
||||
RichContentReceiver { uri ->
|
||||
Log.i("[Rich Edit Text] Received URI: $uri")
|
||||
val activity = context as Activity
|
||||
val sharedViewModel = activity.run {
|
||||
ViewModelProvider(activity as ViewModelStoreOwner)[SharedMainViewModel::class.java]
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
/*
|
||||
* 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.conference.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.adapters.SelectionListAdapter
|
||||
import org.linphone.activities.main.conference.data.ScheduledConferenceData
|
||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||
import org.linphone.databinding.ConferenceScheduleCellBinding
|
||||
import org.linphone.databinding.ConferenceScheduleListHeaderBinding
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.HeaderAdapter
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ScheduledConferencesAdapter(
|
||||
selectionVM: ListTopBarViewModel,
|
||||
private val viewLifecycleOwner: LifecycleOwner
|
||||
) : SelectionListAdapter<ScheduledConferenceData, RecyclerView.ViewHolder>(
|
||||
selectionVM,
|
||||
ConferenceInfoDiffCallback()
|
||||
),
|
||||
HeaderAdapter {
|
||||
val copyAddressToClipboardEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val joinConferenceEvent: MutableLiveData<Event<Pair<String, String?>>> by lazy {
|
||||
MutableLiveData<Event<Pair<String, String?>>>()
|
||||
}
|
||||
|
||||
val editConferenceEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val deleteConferenceInfoEvent: MutableLiveData<Event<ScheduledConferenceData>> by lazy {
|
||||
MutableLiveData<Event<ScheduledConferenceData>>()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScheduledConferencesAdapter.ViewHolder {
|
||||
val binding: ConferenceScheduleCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.conference_schedule_cell,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return ViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
(holder as ScheduledConferencesAdapter.ViewHolder).bind(getItem(position))
|
||||
}
|
||||
|
||||
override fun displayHeaderForPosition(position: Int): Boolean {
|
||||
if (position >= itemCount) return false
|
||||
val conferenceInfo = getItem(position)
|
||||
val previousPosition = position - 1
|
||||
return if (previousPosition >= 0) {
|
||||
val previousItem = getItem(previousPosition)
|
||||
!TimestampUtils.isSameDay(
|
||||
previousItem.conferenceInfo.dateTime,
|
||||
conferenceInfo.conferenceInfo.dateTime
|
||||
)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
override fun getHeaderViewForPosition(context: Context, position: Int): View {
|
||||
val data = getItem(position)
|
||||
val binding: ConferenceScheduleListHeaderBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context),
|
||||
R.layout.conference_schedule_list_header,
|
||||
null,
|
||||
false
|
||||
)
|
||||
binding.title = formatDate(context, data.conferenceInfo.dateTime)
|
||||
binding.executePendingBindings()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun formatDate(context: Context, date: Long): String {
|
||||
if (TimestampUtils.isToday(date)) {
|
||||
return context.getString(R.string.today)
|
||||
}
|
||||
return TimestampUtils.toString(date, onlyDate = true, shortDate = false, hideYear = false)
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
val binding: ConferenceScheduleCellBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(conferenceData: ScheduledConferenceData) {
|
||||
with(binding) {
|
||||
data = conferenceData
|
||||
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
// This is for item selection through ListTopBarFragment
|
||||
selectionListViewModel = selectionViewModel
|
||||
selectionViewModel.isEditionEnabled.observe(
|
||||
viewLifecycleOwner
|
||||
) {
|
||||
position = bindingAdapterPosition
|
||||
}
|
||||
|
||||
setClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||
selectionViewModel.onToggleSelect(bindingAdapterPosition)
|
||||
} else {
|
||||
conferenceData.toggleExpand()
|
||||
}
|
||||
}
|
||||
|
||||
setLongClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == false) {
|
||||
selectionViewModel.isEditionEnabled.value = true
|
||||
// Selection will be handled by click listener
|
||||
true
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
setCopyAddressClickListener {
|
||||
val address = conferenceData.getAddressAsString()
|
||||
if (address.isNotEmpty()) {
|
||||
copyAddressToClipboardEvent.value = Event(address)
|
||||
}
|
||||
}
|
||||
|
||||
setJoinConferenceClickListener {
|
||||
val address = conferenceData.conferenceInfo.uri
|
||||
if (address != null) {
|
||||
joinConferenceEvent.value = Event(
|
||||
Pair(address.asStringUriOnly(), conferenceData.conferenceInfo.subject)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
setEditConferenceClickListener {
|
||||
val address = conferenceData.conferenceInfo.uri
|
||||
if (address != null) {
|
||||
editConferenceEvent.value = Event(address.asStringUriOnly())
|
||||
}
|
||||
}
|
||||
|
||||
setDeleteConferenceClickListener {
|
||||
deleteConferenceInfoEvent.value = Event(conferenceData)
|
||||
}
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ConferenceInfoDiffCallback : DiffUtil.ItemCallback<ScheduledConferenceData>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ScheduledConferenceData,
|
||||
newItem: ScheduledConferenceData
|
||||
): Boolean {
|
||||
return oldItem.conferenceInfo == newItem.conferenceInfo
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ScheduledConferenceData,
|
||||
newItem: ScheduledConferenceData
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
* 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.conference.data
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.contact.GenericContactData
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ConferenceSchedulingParticipantData(
|
||||
val sipAddress: Address,
|
||||
val showLimeBadge: Boolean = false,
|
||||
val showDivider: Boolean = true,
|
||||
val showBroadcastControls: Boolean = false,
|
||||
val speaker: Boolean = false,
|
||||
private val onAddedToSpeakers: ((data: ConferenceSchedulingParticipantData) -> Unit)? = null,
|
||||
private val onRemovedFromSpeakers: ((data: ConferenceSchedulingParticipantData) -> Unit)? = null
|
||||
) :
|
||||
GenericContactData(sipAddress) {
|
||||
val isSpeaker = MutableLiveData<Boolean>()
|
||||
|
||||
val sipUri: String get() = LinphoneUtils.getDisplayableAddress(sipAddress)
|
||||
|
||||
init {
|
||||
isSpeaker.value = speaker
|
||||
}
|
||||
|
||||
fun changeIsSpeaker() {
|
||||
isSpeaker.value = isSpeaker.value == false
|
||||
if (isSpeaker.value == true) {
|
||||
onAddedToSpeakers?.invoke(this)
|
||||
} else {
|
||||
onRemovedFromSpeakers?.invoke(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
/*
|
||||
* 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.conference.data
|
||||
|
||||
class Duration(val value: Int, val display: String) : Comparable<Duration> {
|
||||
override fun toString(): String {
|
||||
return display
|
||||
}
|
||||
|
||||
override fun compareTo(other: Duration): Int {
|
||||
return value.compareTo(other.value)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,193 +0,0 @@
|
|||
/*
|
||||
* 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.conference.data
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.ConferenceInfo
|
||||
import org.linphone.core.ConferenceInfo.State
|
||||
import org.linphone.core.Participant
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ScheduledConferenceData(val conferenceInfo: ConferenceInfo, private val isFinished: Boolean) {
|
||||
val expanded = MutableLiveData<Boolean>()
|
||||
val backgroundResId = MutableLiveData<Int>()
|
||||
|
||||
val address = MutableLiveData<String>()
|
||||
val subject = MutableLiveData<String>()
|
||||
val description = MutableLiveData<String>()
|
||||
val time = MutableLiveData<String>()
|
||||
val date = MutableLiveData<String>()
|
||||
val duration = MutableLiveData<String>()
|
||||
val organizer = MutableLiveData<String>()
|
||||
val canEdit = MutableLiveData<Boolean>()
|
||||
val participantsShort = MutableLiveData<String>()
|
||||
val participantsExpanded = MutableLiveData<String>()
|
||||
val showDuration = MutableLiveData<Boolean>()
|
||||
val isConferenceCancelled = MutableLiveData<Boolean>()
|
||||
val isBroadcast = MutableLiveData<Boolean>()
|
||||
val speakersExpanded = MutableLiveData<String>()
|
||||
|
||||
init {
|
||||
expanded.value = false
|
||||
isBroadcast.value = false
|
||||
|
||||
address.value = conferenceInfo.uri?.asStringUriOnly()
|
||||
subject.value = conferenceInfo.subject
|
||||
description.value = conferenceInfo.description
|
||||
|
||||
time.value = TimestampUtils.timeToString(conferenceInfo.dateTime)
|
||||
date.value = TimestampUtils.toString(
|
||||
conferenceInfo.dateTime,
|
||||
onlyDate = true,
|
||||
shortDate = false,
|
||||
hideYear = false
|
||||
)
|
||||
isConferenceCancelled.value = conferenceInfo.state == State.Cancelled
|
||||
|
||||
val minutes = conferenceInfo.duration
|
||||
val hours = TimeUnit.MINUTES.toHours(minutes.toLong())
|
||||
val remainMinutes = minutes - TimeUnit.HOURS.toMinutes(hours).toInt()
|
||||
duration.value = TimestampUtils.durationToString(hours.toInt(), remainMinutes)
|
||||
showDuration.value = minutes > 0
|
||||
|
||||
val organizerAddress = conferenceInfo.organizer
|
||||
if (organizerAddress != null) {
|
||||
val localAccount = coreContext.core.accountList.find { account ->
|
||||
val address = account.params.identityAddress
|
||||
address != null && organizerAddress.weakEqual(address)
|
||||
}
|
||||
canEdit.value = localAccount != null
|
||||
|
||||
val contact = coreContext.contactsManager.findContactByAddress(organizerAddress)
|
||||
organizer.value = if (contact != null) {
|
||||
contact.name
|
||||
} else {
|
||||
LinphoneUtils.getDisplayName(conferenceInfo.organizer)
|
||||
}
|
||||
} else {
|
||||
canEdit.value = false
|
||||
Log.e(
|
||||
"[Scheduled Conference] No organizer SIP URI found for: ${conferenceInfo.uri?.asStringUriOnly()}"
|
||||
)
|
||||
}
|
||||
|
||||
computeBackgroundResId()
|
||||
computeParticipantsLists()
|
||||
}
|
||||
|
||||
fun destroy() {}
|
||||
|
||||
fun delete() {
|
||||
Log.w(
|
||||
"[Scheduled Conference] Deleting conference info with URI: ${conferenceInfo.uri?.asStringUriOnly()}"
|
||||
)
|
||||
coreContext.core.deleteConferenceInformation(conferenceInfo)
|
||||
}
|
||||
|
||||
fun toggleExpand() {
|
||||
expanded.value = expanded.value == false
|
||||
computeBackgroundResId()
|
||||
}
|
||||
|
||||
fun getAddressAsString(): String {
|
||||
val address = conferenceInfo.uri?.clone()
|
||||
if (address != null) {
|
||||
address.displayName = conferenceInfo.subject
|
||||
return address.asString()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun computeBackgroundResId() {
|
||||
backgroundResId.value = if (conferenceInfo.state == State.Cancelled) {
|
||||
if (expanded.value == true) {
|
||||
R.drawable.shape_round_red_background_with_orange_border
|
||||
} else {
|
||||
R.drawable.shape_round_red_background
|
||||
}
|
||||
} else if (isFinished) {
|
||||
if (expanded.value == true) {
|
||||
R.drawable.shape_round_dark_gray_background_with_orange_border
|
||||
} else {
|
||||
R.drawable.shape_round_dark_gray_background
|
||||
}
|
||||
} else {
|
||||
if (expanded.value == true) {
|
||||
R.drawable.shape_round_gray_background_with_orange_border
|
||||
} else {
|
||||
R.drawable.shape_round_gray_background
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeParticipantsLists() {
|
||||
var participantsListShort = ""
|
||||
var participantsListExpanded = ""
|
||||
var speakersListExpanded = ""
|
||||
|
||||
var allSpeaker = true
|
||||
for (info in conferenceInfo.participantInfos) {
|
||||
val participant = info.address
|
||||
Log.i(
|
||||
"[Scheduled Conference] Conference [${subject.value}] participant [${participant.asStringUriOnly()}] is a [${info.role}]"
|
||||
)
|
||||
|
||||
val contact = coreContext.contactsManager.findContactByAddress(participant)
|
||||
val name = if (contact != null) {
|
||||
contact.name
|
||||
} else {
|
||||
LinphoneUtils.getDisplayName(participant)
|
||||
}
|
||||
val address = participant.asStringUriOnly()
|
||||
participantsListShort += "$name, "
|
||||
when (info.role) {
|
||||
Participant.Role.Listener -> {
|
||||
participantsListExpanded += "$name ($address)\n"
|
||||
allSpeaker = false
|
||||
}
|
||||
else -> { // For meetings created before 5.3 SDK, a speaker might be Unknown
|
||||
speakersListExpanded += "$name ($address)\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
participantsListShort = participantsListShort.dropLast(2)
|
||||
participantsListExpanded = participantsListExpanded.dropLast(1)
|
||||
speakersListExpanded = speakersListExpanded.dropLast(1)
|
||||
|
||||
participantsShort.value = participantsListShort
|
||||
// If all participants have Speaker role then it is a meeting, else it is a broadcast
|
||||
if (!allSpeaker) {
|
||||
participantsExpanded.value = participantsListExpanded
|
||||
speakersExpanded.value = speakersListExpanded
|
||||
isBroadcast.value = true
|
||||
} else {
|
||||
participantsExpanded.value = speakersListExpanded
|
||||
isBroadcast.value = false
|
||||
}
|
||||
Log.i(
|
||||
"[Scheduled Conference] Conference [${subject.value}] is a ${if (allSpeaker) "meeting" else "broadcast"}"
|
||||
)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue