6.0.0 cleanup

This commit is contained in:
Sylvain Berfini 2024-07-15 15:55:03 +02:00
parent c627849382
commit 418f9ba4c9
1031 changed files with 0 additions and 87523 deletions

29
.gitignore vendored
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
View file

@ -1 +0,0 @@
/build

View file

@ -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)
}
}

View file

@ -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>

View file

@ -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"
}

View file

@ -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.**

View file

@ -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>

View file

@ -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>

View file

@ -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">&lt;sip:sip.linphone.org;transport=tls&gt;</entry>
<entry name="reg_route" overwrite="true">&lt;sip:sip.linphone.org;transport=tls&gt;</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>

View file

@ -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

View file

@ -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

View file

@ -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()
}
}

View file

@ -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)
}
}
}
}
}

View file

@ -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

View file

@ -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
}
}
}
}

View file

@ -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)
}

View file

@ -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()
}
}

View file

@ -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()
}
}
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -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()
}
}
}
}

View file

@ -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)
}
}
}
}

View file

@ -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)
}
}
}
}

View file

@ -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)
}
}
}
}

View file

@ -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()
}
}
}

View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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)
}
}
}
}
}

View file

@ -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()
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -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
}
}
}

View file

@ -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()
}
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -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()
)
}
}

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -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
}
}

View file

@ -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")
}
}
}
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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 its 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 isnt 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
}
}

View file

@ -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
)

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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)
)
}
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}

View file

@ -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"
}
}
}

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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)
}

View file

@ -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()
}
}

View file

@ -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
}
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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()
)
}
}

View file

@ -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)
}
}

View file

@ -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
}
}
}

View file

@ -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()
}
}

View file

@ -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()
}
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -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)
}
}

View file

@ -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